aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThéo de la Hogue2023-05-10 15:10:40 +0200
committerThéo de la Hogue2023-05-10 15:10:40 +0200
commitc8b6647d4d8390655e8f57baef3e6557e5fd605a (patch)
tree64f7f300f74907afa397efcb77d7e58ae118d085
parenta125dab309928900b89af7983c2a744b34260592 (diff)
downloadargaze-c8b6647d4d8390655e8f57baef3e6557e5fd605a.zip
argaze-c8b6647d4d8390655e8f57baef3e6557e5fd605a.tar.gz
argaze-c8b6647d4d8390655e8f57baef3e6557e5fd605a.tar.bz2
argaze-c8b6647d4d8390655e8f57baef3e6557e5fd605a.tar.xz
Adding new identification algorithm.
-rw-r--r--src/argaze/GazeAnalysis/VelocityThresholdIdentification.py180
-rw-r--r--src/argaze/GazeAnalysis/__init__.py2
2 files changed, 181 insertions, 1 deletions
diff --git a/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py b/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py
new file mode 100644
index 0000000..5e6ab26
--- /dev/null
+++ b/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python
+
+from typing import TypeVar, Tuple
+from dataclasses import dataclass, field
+import math
+
+from argaze import GazeFeatures
+
+import numpy
+
+FixationType = TypeVar('Fixation', bound="Fixation")
+# Type definition for type annotation convenience
+
+SaccadeType = TypeVar('Saccade', bound="Saccade")
+# Type definition for type annotation convenience
+
+@dataclass(frozen=True)
+class Fixation(GazeFeatures.Fixation):
+ """Define dispersion based fixation."""
+
+ deviation_max: float = field(init=False)
+ """Maximal gaze position distance to the centroïd."""
+
+ def __post_init__(self):
+
+ super().__post_init__()
+
+ points = self.positions.values()
+ points_x, points_y = [p[0] for p in points], [p[1] for p in points]
+ points_array = numpy.column_stack([points_x, points_y])
+ centroid_array = numpy.array([numpy.mean(points_x), numpy.mean(points_y)])
+ deviations_array = numpy.sqrt(numpy.sum((points_array - centroid_array)**2, axis=1))
+
+ # Update frozen focus attribute using centroid
+ object.__setattr__(self, 'focus', (centroid_array[0], centroid_array[1]))
+
+ # Update frozen deviation_max attribute
+ object.__setattr__(self, 'deviation_max', max(deviations_array))
+
+ def point_deviation(self, gaze_position) -> float:
+ """Get distance of a point from the fixation's centroïd."""
+
+ return numpy.sqrt((self.centroid[0] - gaze_position.value[0])**2 + (self.centroid[1] - gaze_position.value[1])**2)
+
+ def overlap(self, fixation) -> bool:
+ """Does a gaze position from another fixation having a deviation to this fixation centroïd smaller than maximal deviation?"""
+
+ points = fixation.positions.values()
+ points_x, points_y = [p[0] for p in points], [p[1] for p in points]
+ points_array = numpy.column_stack([points_x, points_y])
+ centroid_array = numpy.array([self.centroid[0], self.centroid[1]])
+ deviations_array = numpy.sqrt(numpy.sum((points_array - centroid_array)**2, axis=1))
+
+ return min(deviations_array) <= self.deviation_max
+
+ def merge(self, fixation) -> FixationType:
+ """Merge another fixation into this fixation."""
+
+ self.positions.append(fixation.positions)
+ self.__post_init__()
+
+ return self
+
+@dataclass(frozen=True)
+class Saccade(GazeFeatures.Saccade):
+ """Define dispersion based saccade."""
+
+ def __post_init__(self):
+ super().__post_init__()
+
+@dataclass
+class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
+ """Implementation of the I-VT algorithm as described in:
+
+ Dario D. Salvucci and Joseph H. Goldberg. 2000. Identifying fixations and
+ saccades in eye-tracking protocols. In Proceedings of the 2000 symposium
+ on Eye tracking research & applications (ETRA '00). ACM, New York, NY, USA,
+ 71-78. [DOI=http://dx.doi.org/10.1145/355017.355028](DOI=http://dx.doi.org/10.1145/355017.355028)
+ """
+
+ velocity_max_threshold: int|float
+ """Maximal velocity allowed to consider a gaze movement as a fixation."""
+
+ duration_min_threshold: int|float
+ """Minimal duration allowed to consider a gaze movement as a fixation.
+ It is also used as maximal duration allowed to consider a gaze movement as a saccade."""
+
+ def __post_init__(self):
+
+ self.__last_ts = -1
+ self.__last_position = None
+
+ self.__fixation_positions = GazeFeatures.TimeStampedGazePositions()
+ self.__saccade_positions = GazeFeatures.TimeStampedGazePositions()
+
+ def identify(self, ts, gaze_position, terminate=False):
+ """Identify gaze movement from successive timestamped gaze positions.
+
+ The optional *terminate* argument allows to notify identification algorithm that given gaze position will be the last one.
+ """
+
+ # Ignore non valid gaze position
+ if not gaze_position.valid:
+
+ return None if not terminate else self.current_fixation
+
+ # Store first valid position
+ if self.__last_ts < 0:
+
+ self.__last_ts = ts
+ self.__last_position = gaze_position
+
+ return
+
+ # Check if too much time elapsed since last gaze position
+ if (ts - self.__last_ts) > self.duration_min_threshold:
+
+ # Clear all former gaze positions
+ self.__last_ts = ts
+ self.__last_position = gaze_position
+
+ self.__fixation_positions = GazeFeatures.TimeStampedGazePositions()
+ self.__saccade_positions = GazeFeatures.TimeStampedGazePositions()
+
+ return
+
+ # Velocity
+ velocity = gaze_position.distance(self.__last_position) / (ts - self.__last_ts)
+
+ # Remember last position
+ self.__last_ts = ts
+ self.__last_position = gaze_position
+
+ # Velocity is greater than threshold
+ if velocity > self.velocity_max_threshold:
+
+ # Append to saccade positions
+ self.__saccade_positions[ts] = gaze_position
+
+ # Does last fixation exist?
+ if len(self.__fixation_positions) > 0:
+
+ last_fixation = Fixation(self.__fixation_positions)
+
+ # Clear fixation positions
+ self.__fixation_positions = GazeFeatures.TimeStampedGazePositions()
+
+ # Output last fixation
+ return last_fixation if not terminate else self.current_saccade
+
+ # Velocity is less or equals to threshold
+ else:
+
+ # Append to fixation positions
+ self.__fixation_positions[ts] = gaze_position
+
+ # Does last saccade exist?
+ if len(self.__saccade_positions) > 0:
+
+ last_saccade = Saccade(self.__saccade_positions)
+
+ # Clear fixation positions
+ self.__saccade_positions = GazeFeatures.TimeStampedGazePositions()
+
+ # Output last saccade
+ return last_saccade if not terminate else self.current_fixation
+
+ @property
+ def current_fixation(self) -> FixationType:
+
+ if len(self.__fixation_positions) > 0:
+
+ return Fixation(self.__fixation_positions)
+
+ @property
+ def current_saccade(self) -> SaccadeType:
+
+ if len(self.__saccade_positions) > 0:
+
+ return Saccade(self.__saccade_positions)
diff --git a/src/argaze/GazeAnalysis/__init__.py b/src/argaze/GazeAnalysis/__init__.py
index 287425f..7603693 100644
--- a/src/argaze/GazeAnalysis/__init__.py
+++ b/src/argaze/GazeAnalysis/__init__.py
@@ -2,4 +2,4 @@
.. include:: README.md
"""
__docformat__ = "restructuredtext"
-__all__ = ['DispersionThresholdIdentification', 'TransitionProbabilityMatrix'] \ No newline at end of file
+__all__ = ['DispersionThresholdIdentification', 'VelocityThresholdIdentification', 'TransitionProbabilityMatrix'] \ No newline at end of file