Tobii Pro SDK Python API
calibration_during_gaze_recording.py
1 """
2 This example is more comprehensive than most, as it aims to illustrate
3 how you can put together a complete, ready-to-run script, by combining
4 information from different examples. Note that running this script
5 will produce a small CSV data file in the parent directory.
6 """
7 import csv
8 import logging
9 import time
10 import sys
11 
12 import tobii_research as tr
13 
14 # <BeginExample>
15 # Enter the address to the desired eye tracker if you have
16 # more than one (otherwise leave this as an empty string).
17 # (example address: "tobii-prp://TPFC2-100123456789")
18 EYETRACKER_ADDRESS = ""
19 
20 # This list will be filled with dictionaries, with one dictionary per
21 # recording sample.
22 gaze_data_samples = []
23 
24 
25 def get_eyetracker():
26  # Check if a specific eye tracker address has been provided, and if so,
27  # try to locate it and return the corresponding eye tracker object.
28  if EYETRACKER_ADDRESS:
29  eyetracker = tr.EyeTracker(EYETRACKER_ADDRESS)
30  if not eyetracker:
31  sys.exit("Specified eye tracker not found, please check the address.")
32  return eyetracker
33  # If we reach this point, no specific address was provided, so return
34  # the first found eye tracker.
35  all_eyetrackers = tr.find_all_eyetrackers()
36  if not all_eyetrackers:
37  sys.exit(
38  "No connected eye trackers found. Please check the connection "
39  "and/or install any missing drivers with Tobii Pro Eye Tracker Manager."
40  )
41  return all_eyetrackers[0]
42 
43 
44 def gaze_data_callback(gaze_data):
45  # Note that no data processing or saving to file is done here,
46  # as stream callbacks should normally be kept minimal (to ensure
47  # that the system is able to keep up with the eye tracker)
48  # unless you need to e.g. provide real-time feedback.
49  global gaze_data_samples
50  gaze_data_samples.append(gaze_data)
51 
52 
53 def buffer_overflow_notifications_callback(notification_data):
54  logging.error(
55  f"Buffer overflow occurred at system time: {notification_data['system_time_stamp']}"
56  )
57 
58 
59 def calibrate(eyetracker):
60  calibration = tr.ScreenBasedCalibration(eyetracker)
61 
62  # Enter calibration mode.
63  calibration.enter_calibration_mode()
64  print("Entered calibration mode for eye tracker with serial number {0}.".format(eyetracker.serial_number))
65 
66  # Define the points on screen we should calibrate at.
67  # The coordinates are normalized, i.e. (0.0, 0.0) is the upper left corner and (1.0, 1.0) is the lower right corner.
68  points_to_calibrate = [(0.5, 0.5), (0.1, 0.1), (0.1, 0.9), (0.9, 0.1), (0.9, 0.9)]
69 
70  for point in points_to_calibrate:
71  # Normally, you would use a Python package like pygame, PsychoPy or PIL to present an image
72  # indicating the calibration point here.
73  print("Show a point on screen at {0}.".format(point))
74 
75  # Wait a little for user to focus.
76  time.sleep(0.7)
77 
78  print("Collecting data at {0}.".format(point))
79  if calibration.collect_data(point[0], point[1]) != tr.CALIBRATION_STATUS_SUCCESS:
80  # Try again if it didn't go well the first time.
81  # Not all eye tracker models will fail at this point, but instead fail on ComputeAndApply.
82  calibration.collect_data(point[0], point[1])
83 
84  print("Computing and applying calibration.")
85  calibration_result = calibration.compute_and_apply()
86  print("Compute and apply returned {0} and collected at {1} points.".
87  format(calibration_result.status, len(calibration_result.calibration_points)))
88 
89  # Analyze the data and maybe remove points that weren't good.
90  recalibrate_point = (0.1, 0.1)
91  print("Removing calibration point at {0}.".format(recalibrate_point))
92  calibration.discard_data(recalibrate_point[0], recalibrate_point[1])
93 
94  # Redo collection at the discarded point
95  print("Show a point on screen at {0}.".format(recalibrate_point))
96  calibration.collect_data(recalibrate_point[0], recalibrate_point[1])
97 
98  # Compute and apply again.
99  print("Computing and applying calibration.")
100  calibration_result = calibration.compute_and_apply()
101  print("Compute and apply returned {0} and collected at {1} points.".
102  format(calibration_result.status, len(calibration_result.calibration_points)))
103 
104  # See that you're happy with the result.
105 
106  # The calibration is done. Leave calibration mode.
107  calibration.leave_calibration_mode()
108 
109  print("Left calibration mode.")
110 
111 
112 def save_gaze_data(gaze_samples_list):
113  if not gaze_samples_list:
114  print("No gaze samples were collected. Skipping saving")
115  return
116  # To show what kinds of data are available in each sample's dictionary,
117  # we print the available keys here.
118  print("Sample dictionary keys:", gaze_samples_list[0].keys())
119  # This is meant to serve as a simple example of how you can save
120  # some of the gaze data - check the keys to see what else is available.
121  file_handle = open("my_gaze_data.csv", "w")
122  gaze_writer = csv.writer(file_handle)
123  gaze_writer.writerow(["time_seconds", "left_x", "left_y", "right_x", "right_y"])
124  start_time = gaze_samples_list[0]["system_time_stamp"]
125  for recording_dict in gaze_samples_list:
126  sample_time_from_start = recording_dict["system_time_stamp"] - start_time
127  # convert from microseconds to seconds
128  sample_time_from_start = sample_time_from_start / (10**(6))
129  # x is horizontal coordinate on the screen
130  # y is vertical coordinate on the screen
131  left_x, left_y = recording_dict["left_gaze_point_on_display_area"]
132  right_x, right_y = recording_dict["right_gaze_point_on_display_area"]
133  gaze_writer.writerow(
134  [sample_time_from_start, left_x, left_y, right_x, right_y]
135  )
136  file_handle.close()
137 
138 
139 def main():
140  eyetracker = get_eyetracker()
141  # Start gaze recording.
142  print("Subscribing to gaze data for eye tracker with serial number {0}.".format(eyetracker.serial_number))
143  eyetracker.subscribe_to(tr.EYETRACKER_GAZE_DATA, gaze_data_callback, as_dictionary=True)
144  # listen for buffer overflow notifications (to see e.g. if the gaze callback is too slow)
145  eyetracker.subscribe_to(
146  tr.EYETRACKER_NOTIFICATION_STREAM_BUFFER_OVERFLOW,
147  buffer_overflow_notifications_callback,
148  as_dictionary=True
149  )
150  calibrate(eyetracker)
151  # End gaze recording.
152  print("Unsubscribing from gaze data for eye tracker with serial number {0}.".format(eyetracker.serial_number))
153  eyetracker.unsubscribe_from(tr.EYETRACKER_GAZE_DATA, gaze_data_callback)
154  # Save the data, now that all recording is finished.
155  save_gaze_data(gaze_data_samples)
156 
157 # <EndExample>
158 
159 
160 # This is a Python convention used with scripts that are to be run directly
161 # (rather than only being used by other scripts).
162 if __name__ == "__main__":
163  main()