Platform for trajectory tracking experiments & data analysis
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:
-
Several "trajectory generator" that define the movement path (LineTrajectoryGenerator and CircularTrajectoryGenerator to define several segments of the shape's movement trajectory, and SegmentedTrajectoryGenerator to combine these segments).
-
A StimulusAnimator object to animate the square along that predefined trajectory.
​
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()