Skip to content

Instantly share code, notes, and snippets.

@TreyBastian
Created March 15, 2016 16:50
Show Gist options
  • Save TreyBastian/dc5d01d141cc7c3acfe0 to your computer and use it in GitHub Desktop.
Save TreyBastian/dc5d01d141cc7c3acfe0 to your computer and use it in GitHub Desktop.
Calculate Saccades
func calculateVelocities(data []float64, timeData []int64) []float64 {
var result = make([]float64, 0)
for d := range data {
// first 2 points of data set will always be NAN as +2 and -2 don't exist
if d < 2 || d >= len(data)-2 {
result = append(result, math.NaN())
} else {
//calculate delta time over the range - d+2 - d-2 should cover the whole time range
deltaTime := timeData[d+2] - timeData[d-2]
x := (data[d+2] + data[d+1] - data[d-1] - data[d-2]) / (6 * float64(deltaTime))
result = append(result, x)
}
}
return result
}
func DetectSaccades(gazes []database.Gaze, lambda int) ([]Saccade, error) {
// reusable variables to setup
totalGazesAmmount := len(gazes)
gazesX := make([]float64, totalGazesAmmount)
gazesY := make([]float64, totalGazesAmmount)
gazesTime := make([]int64, totalGazesAmmount)
for g := range gazes {
gazesX[g] = gazes[g].X
gazesY[g] = gazes[g].X
gazesTime[g] = gazes[g].Time
}
// first thing we need to do is get the velocities
velocityX := calculateVelocities(gazesX, gazesTime)
velocityY := calculateVelocities(gazesY, gazesTime)
// Now we get the median velocities for x and y while stripping NAN valus and do some math that I'm not sure what it does, but gives us a number we want
// based off hithub.com/tmalsburg/saccades/saccades/R/saccade_recognition.R & Engbert & Kliegl (2003)
// sadly we have to do this in more variables than I wanted to
medianXSqr, err := stats.Median(sqrSlice(stripNaN(velocityX))) // get median of every element squared
if err != nil {
return nil, err
}
medianXN, err := stats.Median(stripNaN(velocityX))
if err != nil {
return nil, err
}
medianX := math.Abs(medianXSqr - math.Pow(medianXN, 2))
medianYSqr, err := stats.Median(sqrSlice(stripNaN(velocityY))) // get median of every element squared
if err != nil {
return nil, err
}
medianYN, err := stats.Median(stripNaN(velocityY))
if err != nil {
return nil, err
}
medianY := math.Abs(medianYSqr - math.Pow(medianYN, 2))
// Next we get the threshholds to detect that it is in fact a saccade
thresholdX := float64(lambda) * medianX
thresholdY := float64(lambda) * medianY
// Now we actually detect gazes that are saccades -- these aren't the saccades we return
var sacs = make([]int, 0)
for i := 0; i < totalGazesAmmount; i++ {
if (math.Pow(velocityX[i]/thresholdX, 2) + math.Pow(velocityY[i]/thresholdY, 2)) > 1 {
sacs = append(sacs, i)
}
}
// Finally we sort the saccades into actual saccades to get duration / start - end points and return
sacsCount := len(sacs)
var saccades = make([]Saccade, 0)
duration := 0
gazeA := 1
for i := 0; i < sacsCount-1; i++ {
if sacs[i+1]-sacs[i] == 1 {
duration++
} else {
if duration >= 3 { // default samples number for Engbert & Kliegl (2003)
var s Saccade
s.StartGaze = sacs[gazeA]
s.EndGaze = sacs[i]
saccades = append(saccades, s)
}
gazeA = i + 1
duration = 1
}
}
return saccades, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment