import numpy as np import sys import math from config import names as gs def get_fixation_list(gaze, errors, xi, yi, ti, fixation_radius_threshold, fixation_duration_threshold, pupil_diameter): n, m = gaze.shape fixations = [] fixation = [] # single fixation, to be appended to fixations counter = 0 # number of points in the fixation sumx = 0 # used to compute the center of a fixation in x and y direction sumy = 0 distance = 0 # captures the distance of a current sample from the fixation center i = 0 # iterates through the gaze samples while i < n - 1: x = gaze[i, xi] y = gaze[i, yi] if counter == 0: # ignore erroneous samples before a fixation if errors[i]: i += 1 continue centerx = x centery = y else: centerx = np.true_divide(sumx, counter) centery = np.true_divide(sumy, counter) if not errors[i]: # only update distance if the current sample is not erroneous distance = np.sqrt((x - centerx) * (x - centerx) + (y - centery) * (y - centery)) if distance > fixation_radius_threshold: # start new fixation if gaze[(i - 1), ti] - gaze[(i - counter), ti] >= fixation_duration_threshold: start_index = i - counter + 1 end_index = i - 1 - 1 # discard fixations with more than 50% erroneous samples percentage_error = np.sum(errors[start_index:(end_index + 1)]) / float(end_index - start_index) if percentage_error >= 0.5: if errors[i]: i += 1 counter = 0 else: counter = 1 sumx = x sumy = y continue gaze_indices = np.arange(start_index, end_index+1)[np.logical_not(errors[start_index:(end_index + 1)])] start_index = gaze_indices[0] end_index = gaze_indices[-1] gazex = gaze[start_index:(end_index + 1), xi][np.logical_not(errors[start_index:(end_index + 1)])] gazey = gaze[start_index:(end_index + 1), yi][np.logical_not(errors[start_index:(end_index + 1)])] gazet = gaze[start_index:(end_index + 1), ti][np.logical_not(errors[start_index:(end_index + 1)])] # extract fixation characteristics fixation.append(np.mean(gazex)) # 0.-1. mean x,y fixation.append(np.mean(gazey)) fixation.append(np.var(gazex)) # 2-3. var x, y fixation.append(np.var(gazey)) fixation.append(gazet[0]) # 4-5. t_start, t_end fixation.append(gazet[-1]) fixation.append(gaze_indices[0]) # 6-7. index_start, index_end fixation.append(gaze_indices[-1]) ds = ((pupil_diameter[start_index:(end_index+1), 1] + pupil_diameter[start_index:(end_index+1), 2]) / 2.)[np.logical_not(errors[start_index:(end_index+1)])] fixation.append(np.mean(ds)) # 8. mean pupil diameter fixation.append(np.var(ds)) # 9. var pupil diameter succ_dx = gazex[1:] - gazex[:-1] succ_dy = gazey[1:] - gazey[:-1] succ_angles = np.arctan2(succ_dy, succ_dx) fixation.append(np.mean(succ_angles)) # 10 mean successive angle fixation.append(np.var(succ_angles)) # 11 var successive angle fixations.append(fixation) assert len(fixation) == len(gs.fixations_list_labels) # set up new fixation fixation = [] if errors[i]: i += 1 counter = 0 else: counter = 1 sumx = x sumy = y else: if not errors[i]: counter += 1 sumx += x sumy += y i += 1 return fixations def get_saccade_list(gaze, fixations, xi, yi, ti, pupil_diameter, fixation_radius_threshold, errors, saccade_min_velocity, max_saccade_duration): saccades = [] wordbook_string = [] # each movement between two subsequent fixations could be a saccade, but for i in xrange(1, len(fixations)): # ...not if the window is too long duration = float(fixations[i][gs.fix_start_t_i] - fixations[i - 1][gs.fix_end_t_i]) if duration > max_saccade_duration: continue start_index = fixations[i - 1][gs.fix_end_index_i] end_index = fixations[i][gs.fix_start_index_i] gazex = gaze[start_index:(end_index + 1), xi][np.logical_not(errors[start_index:(end_index + 1)])] gazey = gaze[start_index:(end_index + 1), yi][np.logical_not(errors[start_index:(end_index + 1)])] gazet = gaze[start_index:(end_index + 1), ti][np.logical_not(errors[start_index:(end_index + 1)])] dx = np.abs(gazex[1:] - gazex[:-1]) dy = np.abs(gazey[1:] - gazey[:-1]) dt = np.abs(gazet[1:] - gazet[:-1]) # ...not if less than 2 non-errouneous amples are left: if len(dt) < 2: continue distance = np.linalg.norm([dx, dy]) peak_velocity = np.amax(distance / dt) start_x = gazex[0] start_y = gazey[0] end_x = gazex[-1] end_y = gazey[-1] dx = end_x - start_x dy = end_y - start_y # ...not if the amplitude is shorter than a fith of fixation_radius_threshold amplitude = np.linalg.norm([dx, dy]) if amplitude < fixation_radius_threshold / 5.0: continue # ...not if the peak velocity is very low if peak_velocity < saccade_min_velocity: continue percentage_error = np.sum(errors[start_index:(end_index + 1)]) / float(end_index - start_index) # ...not if more than 50% of the data are erroneous if percentage_error >= 0.5: continue else: # found saccade! # compute characteristics of the saccade, like start and end point, amplitude, ... saccade = [] saccade.append(start_x) # 0.-1. start x,y saccade.append(start_y) saccade.append(end_x) # 2-3. end x,y saccade.append(end_y) if dx == 0: radians = 0 else: radians = np.arctan(np.true_divide(dy, dx)) if dx > 0: if dy < 0: radians += (2 * np.pi) else: radians = np.pi + radians saccade.append(radians) # 4. angle saccade.append(fixations[i - 1][gs.fix_end_t_i]) # 5-6. t_start, t_end saccade.append(fixations[i][gs.fix_start_t_i]) saccade.append(start_index) # 7-8. index_start, index_end saccade.append(end_index) ds = (pupil_diameter[start_index:(end_index + 1), 1] + pupil_diameter[start_index:(end_index + 1), 2]) / 2.0 saccade.append(np.mean(ds)) # 9. mean pupil diameter saccade.append(np.var(ds)) # 10. var pupil diameter saccade.append(peak_velocity) # 11. peak velocity saccade.append(amplitude) # 12. amplitude # append character representing this kind of saccade to the wordbook_string which will be used for n-gram features sac_id = get_dictionary_entry_for_saccade(amplitude, fixation_radius_threshold, radians) wordbook_string.append(sac_id) saccades.append(saccade) # assert all saccade characteristics were computed assert len(saccade) == len(gs.saccades_list_labels) return saccades, wordbook_string def get_blink_list(event_strings, gaze, ti): assert len(event_strings) == len(gaze) # detect Blinks blinks = [] blink = [] # single blink, to be appended to blinks i = 0 starti = i blink_started = False while i < len(event_strings) - 1: if event_strings[i] == 'Blink' and not blink_started: # start new blink starti = i blink_started = True elif blink_started and not event_strings[i] == 'Blink': blink.append(gaze[starti, ti]) blink.append(gaze[i - 1, ti]) blink.append(starti) blink.append(i - 1) blinks.append(blink) assert len(blink) == len(gs.blink_list_labels) blink_started = False blink = [] i += 1 return blinks def get_dictionary_entry_for_saccade(amplitude, fixation_radius_threshold, degree_radians): # Saacade Type: small, iff amplitude less than 2 fixation_radius_thresholds # U # O A # N u B # M n b C # L l r R # K j f E # J d F # H G # D degrees = np.true_divide(degree_radians * 180.0, np.pi) if amplitude < 2 * fixation_radius_threshold: d_degrees = degrees / (np.true_divide(90, 4)) if d_degrees < 1: sac_id = 'r' elif d_degrees < 3: sac_id = 'b' elif d_degrees < 5: sac_id = 'u' elif d_degrees < 7: sac_id = 'n' elif d_degrees < 9: sac_id = 'l' elif d_degrees < 11: sac_id = 'j' elif d_degrees < 13: sac_id = 'd' elif d_degrees < 15: sac_id = 'f' elif d_degrees < 16: sac_id = 'r' else: print print 'error! d_degrees cannot be matched to a sac_id for a small saccade ', d_degrees sys.exit(1) else: # large d_degrees = degrees / (np.true_divide(90, 8)) if d_degrees < 1: sac_id = 'R' elif d_degrees < 3: sac_id = 'C' elif d_degrees < 5: sac_id = 'B' elif d_degrees < 7: sac_id = 'A' elif d_degrees < 9: sac_id = 'U' elif d_degrees < 11: sac_id = 'O' elif d_degrees < 13: sac_id = 'N' elif d_degrees < 15: sac_id = 'M' elif d_degrees < 17: sac_id = 'L' elif d_degrees < 19: sac_id = 'K' elif d_degrees < 21: sac_id = 'J' elif d_degrees < 23: sac_id = 'H' elif d_degrees < 25: sac_id = 'D' elif d_degrees < 27: sac_id = 'G' elif d_degrees < 29: sac_id = 'F' elif d_degrees < 31: sac_id = 'E' elif d_degrees < 33: sac_id = 'R' else: print 'error! d_degrees cannot be matched to a sac_id for a large saccade: ', d_degrees sys.exit(1) return sac_id def detect_all(gaze, errors, ti, xi, yi, fixation_radius_threshold=0.01, pupil_diameter=None, event_strings=None, fixation_duration_threshold=0.1, saccade_min_velocity=2, max_saccade_duration=0.1): """ :param gaze: gaze data, typically [t,x,y] :param fixation_radius_threshold: dispersion threshold :param fixation_duration_threshold: temporal threshold :param ti, xi, yi: index data for gaze,i.e. for [t,x,y] ti=0, xi=1, yi=2 :param pupil_diameter: pupil diameters values, same length as gaze :param event_strings: list of events, here provided by SMI. used to extract blink information """ fixations = get_fixation_list(gaze, errors, xi, yi, ti, fixation_radius_threshold, fixation_duration_threshold, pupil_diameter) saccades, wordbook_string = get_saccade_list(gaze, fixations, xi, yi, ti, pupil_diameter, fixation_radius_threshold, errors, saccade_min_velocity, max_saccade_duration) blinks = get_blink_list(event_strings, gaze, ti) return fixations, saccades, blinks, wordbook_string