TrajTracker Experiment: Examples

As tutorial, you can examine the following example programs and try modifying them. We recommend to review the sample programs in the order provided here.

All these sample programs are available in the TrajTracker Github, under the samples directory.

Animation

The following script shows how to animate a visual object on the screen.

A white square moves on the screen, and you should catch it with your finger (the finger position is marked with a green circle). If you're using a mouse, move it around while holding the button clicked.

The program makes use of:

As you can see below, the program contains a main loop that runs once per frame, and updates the positions of the circle and the square.

import expyriment as xpy

from trajtracker.utils import get_time

from expyriment.misc.geometry import XYPoint

 

import trajtracker as ttrk

from trajtracker.movement import *

 

 

xpy.control.defaults.window_mode = False

ttrk.log_to_console = True

 

#===========================================================================================

#              Prepare stimuli

#===========================================================================================

 

# The object that moves: a rectangle

square = xpy.stimuli.Rectangle(size=(10, 10), colour=(255,255,255))

 

#-- The movement path (shaped like a rotated 8; it's composed of two straight lines and two half-circles)

#-- The generator can tell the shape's (x,y) coordinates for each given time point

path_generator = SegmentedTrajectoryGenerator(cyclic=True)

path_generator.add_segment(LineTrajectoryGenerator(start_point=(-200, 100), end_point=(200, -100), duration=1), duration=1)

path_generator.add_segment(CircularTrajectoryGenerator(center=(200, 0), radius=100, full_rotation_duration=2, degrees_at_t0=180, clockwise=False), duration=1)

path_generator.add_segment(LineTrajectoryGenerator(start_point=(200, 100), end_point=(-200, -100), duration=1), duration=1)

path_generator.add_segment(CircularTrajectoryGenerator(center=(-200, 0), radius=100, full_rotation_duration=2, degrees_at_t0=180), duration=1)

 

#-- The "animator" object moves the rectangle along the path defined above

animator = ttrk.movement.StimulusAnimator(animated_object=square, trajectory_generator=path_generator)

 

#-- The circle will follows the finger/mouse

circle = xpy.stimuli.Circle(radius=20, colour=(0,255,0))

 

#-- This text will appear whenever the circle "catches" the square

msg = xpy.stimuli.TextBox("Good!", (100, 50), (-300, 200), text_font="Arial", text_size=20,

                          text_colour=(0, 255, 0))

MAX_DISTANCE_FOR_POSITIVE_FEEDBACK = 50

 

 

#===========================================================================================

#              Run the example

#===========================================================================================

 

#-- Initialize Expyriment

exp = xpy.control.initialize()

xpy.control.start(exp)

if not xpy.misc.is_android_running():

    exp.mouse.show_cursor()

 

#-- This loop runs once per frame

start_time = get_time()

while get_time() - start_time < 30:  # continue the game for 30 seconds

 

    #-- Move the square

    animator.update(get_time() - start_time)

 

    # Stimuli are redrawn on every frame. The first stimulus that we present will clear the screen -

    # this is done by calling stim.present(clear=True).

    # For the remaining stimuli, we will call stim.present(clear=False).

    # i.e. we should remember whether we already cleared the screen or not

    screen_cleared = False

 

    #-- If the finger is touching the screen, display the circle in the finger's position

    if exp.mouse.check_button_pressed(0):

        circle.position = exp.mouse.position

        circle.present(update=False)   # this updates the circle position, but doesn't update the display yet

        screen_cleared = True

 

        # if circle and square are close to each other, display the "Good!" message

        if XYPoint(xy=circle.position).distance(XYPoint(xy=square.position)) <= MAX_DISTANCE_FOR_POSITIVE_FEEDBACK:

            msg.present(clear=False, update=False)

 

    square.present(clear=not screen_cleared) # update the square's position; and update the display (wait 1 frame)

 

 

xpy.control.end()

Stimulus selector

The following script demonstrates how to present a "virtual stimulus" that can change into one of several predefined shapes.

Some functions or classes​ expect a single visual stimulus as an argument. For example, in the previous example we saw how StimulusAnimator can move a single stimulus around the screen.

This sample programs shows how to wrap several visual objects - in this case, a red circle and a green circle - as a single "virtual stimulus", using the StimulusSelector class. The StimulusSelector has an interface that imitates other visual stimuli (e.g., it has a present() method, like all Expyriment's objects, and a "position" property to move it around), so classes such as StimulusAnimator accept it. Nevertheless, the StimulusSelector contains two visual objects, red and green circles, and you can select which of them to actually present at each time.

Again, the program contains a main loop that runs once per frame and updates the StimulusSelector's position. Every now and then, the program also changes the active underlying stimulus from red to green or vice versa.

import numpy as np

import random

 

import expyriment as xpy

import trajtracker as ttrk

from trajtracker.utils import get_time

 

xpy.control.defaults.window_mode = True

ttrk.log_to_console = True

 

 

#-- Initialize Expyriment

exp = xpy.control.initialize()

xpy.control.start(exp)

if not xpy.misc.is_android_running():

    exp.mouse.show_cursor()

 

 

#===========================================================================================

#              Prepare stimuli

#===========================================================================================

 

#-- Create stimuli

selector = ttrk.stimuli.StimulusSelector()

selector.add_stimulus("red", xpy.stimuli.Circle(radius=10, colour=xpy.misc.constants.C_RED))

selector.add_stimulus("green", xpy.stimuli.Circle(radius=10, colour=xpy.misc.constants.C_GREEN))

 

#-- Move them in circles

path_generator = ttrk.movement.CircularTrajectoryGenerator(center=(0,0), radius=200, full_rotation_duration=5)

animator = ttrk.movement.StimulusAnimator(animated_object=selector, trajectory_generator=path_generator)

 

 

#===========================================================================================

#              Run the example

#===========================================================================================

 

red = True

selector.activate("red")

 

 

start_time = get_time()

last_color_change_time = start_time

 

#-- This loop runs once per frame

while get_time() - start_time < 30:  # continue for 30 seconds

 

    #-- Change color in random times

    if get_time() - last_color_change_time > (0.5 + 2 * random.random()):

        red = not red

        selector.activate("red" if red else "green")

        last_color_change_time = get_time()

 

    #-- Move the stimulus

    animator.update(get_time() - start_time)

 

    #-- Update the display

    selector.present()

 

xpy.control.end()

Basic tracking of finger/mouse trajectory

This sample program demonstrates a basic trajectory-tracking experiment.

The program shows a rectangle at the bottom of the screen and a rectangle on top. Start a trial by touching the rectangle (or click it with the mouse without releasing the button). Drag the finger/mouse until it hits the circle. The trajectories of all trials are saved in a file called trajectory_<subject-number>.csv in the "data" sub-directory.

The program uses two TrajTracker components:

  • TrajectoryTracker is the class that tracks the finger movement and saves it in CSV format.

  • RectStartPoint (and the similar but more generic StartPoint) takes care of initiating a trial. In this example, it operates in a mode where the movement initiates the trial (and would typically trigger the appearance of stimuli). Alternatively, the StartPoint can also work in a "stimulus-then-move" mode, where you are expected to start moving your finger in a predefined time, usually after the stimulus appeared.

import numpy as np

import random

 

import expyriment as xpy

import trajtracker as ttrk

from trajtracker.movement import StartPoint

from trajtracker.utils import get_time

 

xpy.control.defaults.window_mode = True

ttrk.log_to_console = True

 

 

#-- Initialize Expyriment

exp = xpy.control.initialize()

xpy.control.start(exp)

if not xpy.misc.is_android_running():

    exp.mouse.show_cursor()

 

 

#===========================================================================================

#              Prepare stimuli

#===========================================================================================

 

N_TRIALS = 20

 

screen_width = exp.screen.size[0]

screen_height = exp.screen.size[1]

 

#-- The START point

start_point = ttrk.movement.RectStartPoint(size=(40, 30), position=(0, -(screen_height-30)/2))

 

#-- The target point that you should reach

target_point = xpy.stimuli.Circle(radius=20, colour=xpy.misc.constants.C_GREEN)

target_point.position = (-50, screen_height/2 - target_point.radius * 1.5)

 

#-- Message displaying no. of completed trials

progress_msg = xpy.stimuli.TextBox("Completed: 0/%d" % N_TRIALS, size=(300, 50), text_font="Arial", text_size=20,

                                   text_colour=xpy.misc.constants.C_WHITE, text_justification=2)

progress_msg.position = ((screen_width - progress_msg.size[0]) / 2, (screen_height - progress_msg.size[1]) / 2)

 

#-- Shows an error when the finger/mouse leaves the start point in the wrong direction

err_msg = xpy.stimuli.TextBox("Please move UPWARDS from the start point!", size=(300, 50),

                              text_font="Arial", text_size=20,

                              text_colour=xpy.misc.constants.C_RED)

 

#-- Tracks & saves the finger trajectory

traj_tracker = ttrk.movement.TrajectoryTracker("data/trajectory_%d.csv" % exp.subject)

 

#-----------------------------------------------------------

#-- Update the display.

def present_stimuli(show_error=False):

 

    start_point.start_area.present(update=False)

 

    target_point.present(clear=False, update=False)

 

    if show_error:

        err_msg.present(clear=False, update=False)

 

    progress_msg.present(clear=False)

 

 

#===========================================================================================

#              Run task

#===========================================================================================

 

traj_tracker.init_output_file()

 

present_stimuli()

 

n_completed = 0

 

while n_completed < N_TRIALS:

 

    start_point.reset()

 

    trial_start_time = get_time()

    traj_tracker.reset(trial_start_time)

 

    # Wait for mouse/finger to click / touch screen

    start_point.wait_until_startpoint_touched(exp)

    present_stimuli()

 

    # Wait for mouse/finger to start moving

    start_point.wait_until_exit(exp)

    if start_point.state == StartPoint.State.aborted:

        # Finger lifted (trial aborted)

        continue

 

    elif start_point.state == StartPoint.State.error:

        # Finger moved sideways rather than upwards: show an error message

        present_stimuli(show_error=True)

        continue

 

    # good! Mouse/finger started moving upwards. Now we wait for it to reach the target

    traj_tracker.enabled = True

 

    # This loop runs once per frame, and stops only when the finger is lifted or reaches the target circle

    while True:

 

        if not exp.mouse.check_button_pressed(0):

            # Finger lifted / mouse unclicked: abort the trial

            break

 

        # The mouse/finger moves

 

        mouse_pos = exp.mouse.position

 

        # save trajectory data

        traj_tracker.update_xyt(mouse_pos, get_time() - trial_start_time)

 

        if target_point.overlapping_with_position(mouse_pos):

            # Reached the target circle!

            n_completed += 1

            traj_tracker.save_to_file(n_completed)

            progress_msg.unload()

            progress_msg.text = "Completed: %d/%d" % (n_completed, N_TRIALS)

            present_stimuli()

            break

 

xpy.control.end()

Monitoring the finger movement

TrajTracker includes two monitor classes - one for continuously monitoring the finger speed, another for monitoring its direction.

In this example script, you need to touch the screen and move your finger around (or move the mouse around without unclicking). The speed and direction of movement are monitored and updated on screen.

import expyriment as xpy

import trajtracker as ttrk

from trajtracker.utils import get_time

 

 

xpy.control.defaults.window_mode = True

ttrk.log_to_console = True

 

 

 

#-- Initialize Expyriment

exp = xpy.control.initialize()

xpy.control.start(exp)

if not xpy.misc.is_android_running():

    exp.mouse.show_cursor()

 

 

#===========================================================================================

#              Prepare

#===========================================================================================

 

#-- monitor speed & direction

speed_monitor = ttrk.movement.SpeedMonitor(1)

direction_monitor = ttrk.movement.DirectionMonitor(min_distance=10, min_angle_change_per_curve=5)

 

 

#------ Create text boxes for presenting the speed & direction information

 

screen_width = exp.screen.size[0]

screen_height = exp.screen.size[1]

 

def create_textbox(distance_from_top):

    box = xpy.stimuli.TextBox("", size=(300, 50), text_font="Arial", text_size=20,

                              text_colour=xpy.misc.constants.C_WHITE, text_justification=0)

    box.position = (-(screen_width - box.size[0]) / 2, (screen_height - box.size[1]) / 2 - distance_from_top)

    return box

 

tb_xspeed = create_textbox(10)

tb_yspeed = create_textbox(70)

tb_angle = create_textbox(130)

tb_curves = create_textbox(190)

 

 

#------------------------------------------------

#-- Update the information in the text boxes according to the monitors

def update_textboxes():

    tb_xspeed.unload()

    tb_yspeed.unload()

    tb_angle.unload()

    tb_curves.unload()

 

    tb_xspeed.text = "X Speed: {:04d} c/s".format(0 if speed_monitor.xspeed is None else int(speed_monitor.xspeed))

    tb_yspeed.text = "Y Speed: {:04d} c/s".format(0 if speed_monitor.yspeed is None else int(speed_monitor.yspeed))

    tb_angle.text = "Direction: {:03d} deg".format(0 if direction_monitor.curr_angle is None \

                       else int(direction_monitor.curr_angle))

    tb_curves.text = "Turns: {:}".format(direction_monitor.n_curves)

 

 

#------------------------------------------------

def update_display():

    tb_xspeed.present(update=False)

    tb_yspeed.present(clear=False, update=False)

    tb_angle.present(clear=False, update=False)

    tb_curves.present(clear=False)

 

 

#===========================================================================================

#              Run the task

#===========================================================================================

 

update_textboxes()

update_display()

 

speed_monitor.reset(0)

direction_monitor.reset()

 

start_time = get_time()

last_updated_texts_time = 0.0

 

while get_time() - start_time < 60:   # continue for 60 sec

 

    curr_time = get_time()

 

    if exp.mouse.check_button_pressed(0):

        #-- Finger is touching the screen

 

        #-- Update the monitors on each frame, so they can continuously track the speed

        #-- and direction of movement

        speed_monitor.update_xyt(exp.mouse.position, curr_time - start_time)

        direction_monitor.update_xyt(exp.mouse.position, curr_time - start_time)

 

        # Update the text information only every 100 ms (because this is time consuming)

        if get_time() - last_updated_texts_time >= 0.1:

            update_textboxes()

 

    else:

        #-- Finger not touching the screen

        speed_monitor.reset(0)

        direction_monitor.reset()

 

    update_display()

 

xpy.control.end()

Advanced movement validations

TrajTracker includes several validators that restrict the finger/mouse movement. The simple validators enforce minimal/maximal speed, or prohibit movement in certain directions. The present sample program demonstrates two validators that restrict the movement in a more accurate manner. The program presents a ring-like shape, and uses the two validators to enforce that you move your finger only inside the ring, and only in a clockwise direction.

Both validators rely on the definition of various screen areas. The screen areas are defined using a bitmap image, which would typically be of the same size as the screen. Note that the image is not actually presented on screen - it is merely used for defining screen areas:

  • LocationsValidator assumes that a screen area is defined by the set of pixels that have the same color in the image. In the sample program, the validator uses this picture of a white ring. The picture has only black and white pixels. The validator allows touching the screen only in locations corresponding with white pixels, thereby restricting the finger movement to be inside the ring.

  • MoveByGradientValidator allows moving only from lighter pixels to darker pixels, or vice versa. The validator uses this picture, which shows a ring of the same size as before, but now the ring has a gradient of blue color that gets lighter when you move clockwise. The validator restricts the movement to darker-to-lighter, thereby enforcing clockwise movement. The out-of-ring regions have white color in this image. Because white is not in the color scales of blue, the validator ignores movement in these regions.

import expyriment as xpy

from trajtracker.utils import get_time

import trajtracker as ttrk

from trajtracker.validators import *

xpy.control.defaults.window_mode = True

ttrk.log_to_console = True

exp = xpy.control.initialize()

xpy.control.start(exp)

if not xpy.misc.is_android_running():

    exp.mouse.show_cursor()

 

 

#-- The ring stimulus

ring = xpy.stimuli.Picture("ring.bmp", position=(0,0))

 

#-- Movement validators

in_ring_validator = LocationsValidator("gradient.bmp", position=(0, 0), default_valid=True)

in_ring_validator.invalid_colors = ((255, 255, 255))

 

direction_validator = MoveByGradientValidator("gradient.bmp", position=(0, 0), cyclic=True, max_valid_back_movement=5)

direction_validator.single_color = "B"   # use only the blue scale

direction_validator.log_level = ttrk.log_debug

 

#-- Messages shown to subject

 

messages_x = exp.screen.size[0] / 2 - 100

messages_y = exp.screen.size[1] / 2 - 50

 

instruction = xpy.stimuli.TextBox("Move clockwise inside the ring", size=(200, 50),

                                  position=(-messages_x, messages_y), text_font="Arial",

                                  text_size=20, text_colour=(255, 255, 255))

 

location_err = xpy.stimuli.TextBox("Out of ring", size=(200, 50),

                                   position=(messages_x, messages_y), text_font="Arial",

                                   text_size=20, text_colour=(255, 0, 0))

 

direction_err = xpy.stimuli.TextBox("Wrong direction", size=(200, 50),

                                   position=(messages_x, messages_y), text_font="Arial",

                                   text_size=20, text_colour=(255, 0, 0))

#============================================================================

ring.present(update=False)

instruction.present(clear=False)

 

button_was_already_pressed = False

 

start_time = get_time()

time = start_time

 

while time - start_time < 30000:  # continue for 30 seconds

 

    ring.present(update=False) # to clear previous stuff

 

    if exp.mouse.check_button_pressed(0):

 

        finger_pos = exp.mouse.position

 

        if button_was_already_pressed:

            # Check movement

            if in_ring_validator.update_xyt(finger_pos, time):

                location_err.present(update=False, clear=False)

                print("Location error")

            elif direction_validator.update_xyt(finger_pos, time):

                direction_err.present(update=False, clear=False)

                print("Direction error")

 

        else:

            # The finger touched the screen just now: reset validators, but don't validate yet

            button_was_already_pressed = True

            in_ring_validator.reset(time)

            direction_validator.reset(time)

 

    else:

        # Finger lifted

        button_was_already_pressed = False

 

    instruction.present(clear=False) # Go to next frame

    time = get_time()

 

 

xpy.control.end()

Number-to-position mapping

This is a simple version of the number-to-position mapping task, containing only the main elements in this task: number line, target numbers, "start" point, and basic movement validations. In this version, no data is saved.

Note that this sample script is different from (and simpler than) the number-to-position paradigm provided as part of TrajTracker.

samples/basic/ImageValidators

import random

import numpy as np

 

import expyriment as xpy

from trajtracker.utils import get_time

 

import trajtracker as ttrk

from trajtracker.movement import StartPoint

 

 

xpy.control.defaults.window_mode = True

ttrk.log_to_console = True

 

#-- Experiment constants

MAX_TRIAL_DURATION = 2

MAX_NUMBERLINE_VALUE = 100

GUIDE_ENABLED = True

N_TRIALS = 20

 

 

#------------------------------------------------

def run_trial():

 

    start_point.reset()

    number_line.reset()    # mark the line as yet-untouched

 

    all_stimuli.present()  # reset the display

 

    #-- Choose target

    target_box.visible = False

    target_box.unload()

    target_box.text = "{:.0f}".format(np.floor(random.random()*(MAX_NUMBERLINE_VALUE+1)))

    target_box.preload()

 

    #-- Wait for the participant to initiate the trial by touching the START point

    start_point.wait_until_startpoint_touched(exp, on_loop_present=all_stimuli)

 

    #-- Clean remains from previous trial

    feedback_arrow.visible = False

    err_textbox.visible = False

    all_stimuli.present()

 

    time0 = get_time()

    reset_trajectory_info(time0)

 

    #-- Wait for the participant to start moving the finger

    start_point.wait_until_exit(exp, on_loop_present=all_stimuli)

    if start_point.state == StartPoint.State.aborted:

        print("   Trial aborted.")

        return False

    elif start_point.state == StartPoint.State.error:

        trial_error("Start the trial by moving upwards, not sideways!")

        return False

 

    #-- Movement started: show target

    target_box.visible = True

    all_stimuli.present()

 

    global_speed_validator.reset(get_time() - time0)   # indicate that time-counting starts now

 

 

    while True:  # This loop runs once per frame

 

        if not exp.mouse.check_button_pressed(0):

            trial_error("Finger lifted in mid-trial")

            return False

 

        err = update_trajectory(exp.mouse.position, get_time() - time0)

        if err is not None:

            trial_error(err.message)

            return False

 

        #-- Handle movement: Check if the number line was reached

        if number_line.touched:

            trial_succeeded()

            return True

 

        all_stimuli.present()  # update display

 

 

#------------------------------------------------

def reset_trajectory_info(trial_start_time):

    for obj in trajectory_sensitive_objects:

        obj.reset(trial_start_time)

 

 

#------------------------------------------------

# Run all validations for the given time point

#

def update_trajectory(finger_position, time_in_trial):

 

    for obj in trajectory_sensitive_objects:

        err = obj.update_xyt(finger_position, time_in_trial)

        if err is not None:

            return err

 

    return None

 

 

#------------------------------------------------

# This function is called when a trial ends with an error

#

def trial_error(err):

 

    print("   ERROR in trial: " + err)

 

    err_textbox.unload()

    err_textbox.text = err

    err_textbox.visible = True

 

    target_box.visible = False

    speed_guide.activate(None)

 

 

#------------------------------------------------

# This function is called when a trial ends with no error

#

def trial_succeeded():

 

    print("   Trial ended successfully.")

 

    target_box.visible = False

 

    feedback_arrow.visible = True

    nl_pos = number_line.position

    feedback_arrow.position = (number_line.response_coord + nl_pos[0], nl_pos[1] + feedback_arrow.height / 2)

 

    speed_guide.activate(None)

 

 

#===========================================================================================

 

#-- Initialize expyriment

 

exp = xpy.control.initialize()

xpy.control.start(exp)

if not xpy.misc.is_android_running():

    exp.mouse.show_cursor()

 

 

#-- Initialize the objects for the number-to-position experiment

#---------------------------------------------------------------

 

all_stimuli = ttrk.stimuli.StimulusContainer("main")

trajectory_sensitive_objects = []

 

#-- Number line

number_line = ttrk.stimuli.NumberLine(position=(0, exp.screen.size[1] / 2 - 80),

                                      line_length=int(exp.screen.size[0] * 0.85),

                                      max_value=MAX_NUMBERLINE_VALUE,

                                      line_colour=xpy.misc.constants.C_WHITE,

                                      end_tick_height=5)

number_line.show_labels(font_name="Arial", font_size=26, box_size=(100, 30), offset=(0, 20),

                        font_colour=xpy.misc.constants.C_GREY)

all_stimuli.add(number_line)

trajectory_sensitive_objects.append(number_line)

 

#-- Feedback arrow

feedback_arrow = xpy.stimuli.Shape()

feedback_arrow.add_vertices([(10, 20), (-6, 0), (0, 20), (-9, 0), (0, -20), (-6, 0)])

feedback_arrow.colour = xpy.misc.constants.C_GREEN

all_stimuli.add(feedback_arrow, visible=False)

 

#-- "Start" area

start_area = xpy.stimuli.Rectangle(size=(40, 30))

start_area.position = (0, - (exp.screen.size[1] / 2 - start_area.size[1] / 2))

all_stimuli.add(start_area)

start_point = StartPoint(start_area)

 

# -- Target number

target_box = xpy.stimuli.TextBox("", (300, 80), (0, exp.screen.size[1] / 2 - 50),

                                 text_font="Arial", text_size=50, text_colour=xpy.misc.constants.C_WHITE)

all_stimuli.add(target_box, visible=False)

 

#-- Validators

direction_validator = \

    trajtracker.validators.MovementAngleValidator(min_angle=-90, max_angle=90, calc_angle_interval=20, enabled=True)

trajectory_sensitive_objects.append(direction_validator)

 

global_speed_validator = \

    trajtracker.validators.GlobalSpeedValidator(origin_coord=start_area.position[1] + start_area.size[1] / 2,

                                                end_coord=number_line.position[1],

                                                grace_period=0.3, max_trial_duration=MAX_TRIAL_DURATION,

                                                milestones=[(.5, .33), (.5, .67)], show_guide=GUIDE_ENABLED)

global_speed_validator.do_present_guide = False

trajectory_sensitive_objects.append(global_speed_validator)

speed_guide = global_speed_validator.guide.stimulus

if GUIDE_ENABLED:

    all_stimuli.add(speed_guide)

 

#-- Error message

err_textbox = xpy.stimuli.TextBox("", (290, 180), (0, 0),

                                  text_font="Arial", text_size=16, text_colour=xpy.misc.constants.C_RED)

all_stimuli.add(err_textbox, "err_box", visible=False)

 

 

#-- Run the experiment

n_trials_completed = 0

while n_trials_completed < N_TRIALS:

    if run_trial():

        n_trials_completed += 1

 

 

xpy.control.end()