328 lines
9.9 KiB
Python
328 lines
9.9 KiB
Python
|
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
|