aboutsummaryrefslogtreecommitdiff
path: root/src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py')
-rw-r--r--src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py112
1 files changed, 60 insertions, 52 deletions
diff --git a/src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py b/src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py
index e205189..94b6357 100644
--- a/src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py
+++ b/src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py
@@ -11,15 +11,18 @@ import numpy
class Fixation(GazeFeatures.Fixation):
"""Define dispersion based fixation."""
- dispersion: float = field(init=False)
- """Dispersion of all gaze positions belonging to the fixation."""
+ centroid: tuple = field(init=False)
+ """Centroïd of all gaze positions belonging to the fixation."""
+
+ deviation_max: float = field(init=False)
+ """Maximal gaze position distance to the centroïd."""
+
+ deviation_mean: float = field(init=False)
+ """Average of gaze position distances to the centroïd."""
euclidian: bool = field(default=True)
"""Does the distance is calculated in euclidian way."""
- centroid: tuple = field(init=False)
- """Centroïd of all gaze positions belonging to the fixation."""
-
def __post_init__(self):
super().__post_init__()
@@ -44,25 +47,29 @@ class Fixation(GazeFeatures.Fixation):
dist = numpy.sum(dist, axis=1)
dist = numpy.sqrt(dist)
- __dispersion = max(dist)
+ __deviation_max = max(dist)
+ __deviation_mean = numpy.mean(dist)
else:
- __dispersion = (max(x_list) - min(x_list)) + (max(y_list) - min(y_list))
-
- # Update frozen dispersion attribute
- object.__setattr__(self, 'dispersion', __dispersion)
+ __deviation_max = (max(x_list) - min(x_list)) + (max(y_list) - min(y_list))
# Update frozen centroid attribute
object.__setattr__(self, 'centroid', (cx, cy))
+ # Update frozen deviation_max attribute
+ object.__setattr__(self, 'deviation_max', __deviation_max)
+
+ # Update frozen deviation_mean attribute
+ object.__setattr__(self, 'deviation_mean', __deviation_mean)
+
def overlap(self, fixation) -> float:
"""Does this fixation overlap another fixation?"""
dist = (self.centroid[0] - fixation.centroid[0])**2 + (self.centroid[1] - fixation.centroid[1])**2
dist = numpy.sqrt(dist)
- return dist < (self.dispersion + fixation.dispersion)
+ return dist <= (self.deviation_max + fixation.deviation_max)
def contains_point(self, gaze_position) -> bool:
"""Is a point inside fixation?"""
@@ -70,7 +77,7 @@ class Fixation(GazeFeatures.Fixation):
dist = (self.centroid[0] - gaze_position[0])**2 + (self.centroid[1] - gaze_position[1])**2
dist = numpy.sqrt(dist)
- return dist < self.dispersion
+ return dist <= self.deviation_max
def merge(self, fixation) -> float:
"""Merge another fixation into this fixation."""
@@ -85,13 +92,6 @@ class Saccade(GazeFeatures.Saccade):
def __post_init__(self):
super().__post_init__()
-@dataclass(frozen=True)
-class UnknownGazeMovement(GazeFeatures.UnknownGazeMovement):
- """Define dispersion based unknown movement."""
-
- def __post_init__(self):
- super().__post_init__()
-
@dataclass
class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
"""Implementation of the I-DT algorithm as described in:
@@ -102,11 +102,12 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
71-78. [DOI=http://dx.doi.org/10.1145/355017.355028](DOI=http://dx.doi.org/10.1145/355017.355028)
"""
- dispersion_threshold: int|float
- """Maximal distance allowed to consider several gaze positions as a fixation."""
+ deviation_max_threshold: int|float
+ """Maximal distance allowed to consider a gaze movement as a fixation."""
- duration_threshold: int|float
- """Minimal duration allowed to consider several gaze positions 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 __iter__(self) -> GazeFeatures.GazeMovementType:
"""GazeMovement identification generator."""
@@ -123,6 +124,15 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
while not gaze_position_current.valid and len(self.__ts_gaze_positions) > 0:
ts_current, gaze_position_current = self.__ts_gaze_positions.pop_first()
+ # Output last fixation after too much unvalid positons
+ if self.__last_fixation != None:
+
+ ts_last, gaze_position_last = self.__last_fixation.positions.last
+
+ if (ts_current - ts_last) > self.duration_min_threshold:
+
+ yield self.__last_fixation
+
# Select current and next valid gaze positions until a duration threshold
valid_gaze_positions = GazeFeatures.TimeStampedGazePositions()
valid_gaze_positions[ts_current] = gaze_position_current
@@ -132,7 +142,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
for ts_next, gaze_position_next in self.__ts_gaze_positions.items():
- if (ts_next - ts_current) < self.duration_threshold:
+ if (ts_next - ts_current) < self.duration_min_threshold:
# Store valid position
if gaze_position_next.valid:
@@ -155,7 +165,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
new_fixation = Fixation(valid_gaze_positions)
# Dispersion small enough: it is a fixation ! Try to extend it
- if new_fixation.dispersion <= self.dispersion_threshold:
+ if new_fixation.deviation_max <= self.deviation_max_threshold:
# Remove valid and unvalid gaze positions as there as now stored in new fixation
# -1 as current gaze position have already been poped
@@ -181,11 +191,11 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
extended_fixation = Fixation(extended_gaze_positions)
# Dispersion is too wide : break
- if extended_fixation.dispersion > self.dispersion_threshold:
+ if extended_fixation.deviation_max > self.deviation_max_threshold:
break
- # NOTE : The last extended position is out of the fixation : this position will be popped later
+ # NOTE: The last extended position is out of the fixation : this position will be popped later
# Update new fixation
new_fixation = extended_fixation
@@ -207,7 +217,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
self.__last_fixation.update()
new_fixation.update()
- # Edit inter movement gaze positions
+ # Edit inter fixations movement gaze positions
movement_gaze_positions = GazeFeatures.TimeStampedGazePositions()
# Edit first movement gaze position
@@ -234,34 +244,23 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
# Edit last movement gaze position
movement_gaze_positions[stop_movement_ts] = stop_position
- # Short time between fixations :
- # this movement is a saccade unless last and new fixations overlap
- if stop_movement_ts - start_movement_ts <= self.duration_threshold:
-
- # Does new fixation overlap last fixation?
- if self.__last_fixation.overlap(new_fixation):
+ # End of inter fixations movement edition
- merged_positions = self.__last_fixation.positions
- merged_positions.append(movement_gaze_positions)
- merged_positions.append(new_fixation.positions)
+ # Does new fixation overlap last fixation?
+ if self.__last_fixation.overlap(new_fixation):
- self.__last_fixation = Fixation(merged_positions)
+ # Merge new fixation into last fixation
+ self.__last_fixation.merge(new_fixation)
- # Forget new fixation
- new_fixation = None
+ # QUESTION: What to do if the time between the two fixations it very long ?
+ # It would be dangerous to set a timeout value as a fixation duration has no limit.
- else:
-
- # Output last fixation
- yield self.__last_fixation
+ # Forget new fixation
+ new_fixation = None
- # New fixation becomes the last fixation to allow further merging
- self.__last_fixation = new_fixation
+ # NOTE: Ignore inter fixations gaze positions: there was probably noisy positions.
- # Output saccade
- yield Saccade(movement_gaze_positions)
-
- # Too much time between fixations: this movement is unknown
+ # Otherwise,
else:
# Output last fixation
@@ -270,8 +269,17 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier):
# New fixation becomes the last fixation to allow further merging
self.__last_fixation = new_fixation
- # Output unknown movement
- yield UnknownGazeMovement(movement_gaze_positions)
+ # Short time between fixations : this movement is a saccade
+ if stop_movement_ts - start_movement_ts <= self.duration_min_threshold:
+
+ # Output saccade
+ yield Saccade(movement_gaze_positions)
+
+ # Too much time between fixations: this movement is unknown
+ else:
+
+ # Output unknown movement
+ yield GazeFeatures.GazeMovement(movement_gaze_positions)
# In any case, forget former unmatched gaze positions
unmatched_gaze_positions = GazeFeatures.TimeStampedGazePositions()