diff options
Diffstat (limited to 'src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py')
-rw-r--r-- | src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py | 112 |
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() |