diff options
author | Théo de la Hogue | 2023-05-10 11:16:04 +0200 |
---|---|---|
committer | Théo de la Hogue | 2023-05-10 11:16:04 +0200 |
commit | 0abc57e6ee22ec6018a4c0799bbf499fa7749877 (patch) | |
tree | 2346ff86cef90384da57524797e46ec9126f15fe | |
parent | d0d05aed72759b0fcbb15ed3895636ef3c0c2611 (diff) | |
download | argaze-0abc57e6ee22ec6018a4c0799bbf499fa7749877.zip argaze-0abc57e6ee22ec6018a4c0799bbf499fa7749877.tar.gz argaze-0abc57e6ee22ec6018a4c0799bbf499fa7749877.tar.bz2 argaze-0abc57e6ee22ec6018a4c0799bbf499fa7749877.tar.xz |
Renaming file.
-rw-r--r-- | src/argaze.test/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py | 32 | ||||
-rw-r--r-- | src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py | 189 |
2 files changed, 16 insertions, 205 deletions
diff --git a/src/argaze.test/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py b/src/argaze.test/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py index 122e2d8..3de7935 100644 --- a/src/argaze.test/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py +++ b/src/argaze.test/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py @@ -5,7 +5,7 @@ import random import time from argaze import GazeFeatures -from argaze.GazeAnalysis import DispersionBasedGazeMovementIdentifier +from argaze.GazeAnalysis import DispersionThresholdIdentification import numpy @@ -81,11 +81,11 @@ def build_gaze_saccade(size: int, center_A: tuple, center_B: tuple, start_time: return ts_gaze_positions -class TestDispersionBasedGazeMovementIdentifierClass(unittest.TestCase): - """Test DispersionBasedGazeMovementIdentifier class.""" +class TestDispersionThresholdIdentificationClass(unittest.TestCase): + """Test DispersionThresholdIdentification class.""" def test_fixation_identification(self): - """Test DispersionBasedGazeMovementIdentifier fixation identification.""" + """Test DispersionThresholdIdentification fixation identification.""" size = 10 center = (0, 0) @@ -95,7 +95,7 @@ class TestDispersionBasedGazeMovementIdentifierClass(unittest.TestCase): max_time = 0.1 ts_gaze_positions = build_gaze_fixation(size, center, deviation_max, start_time, min_time, max_time) - gaze_movement_identifier = DispersionBasedGazeMovementIdentifier.GazeMovementIdentifier(deviation_max_threshold=deviation_max, duration_min_threshold=max_time*2) + gaze_movement_identifier = DispersionThresholdIdentification.GazeMovementIdentifier(deviation_max_threshold=deviation_max, duration_min_threshold=max_time*2) ts_fixations, ts_saccades, ts_status = gaze_movement_identifier.browse(ts_gaze_positions) # Check result size @@ -112,7 +112,7 @@ class TestDispersionBasedGazeMovementIdentifierClass(unittest.TestCase): self.assertLessEqual(fixation.duration, size * max_time) def test_fixation_and_direct_saccade_identification(self): - """Test DispersionBasedGazeMovementIdentifier fixation and saccade identification.""" + """Test DispersionThresholdIdentification fixation and saccade identification.""" size = 10 center_A = (0, 0) @@ -127,7 +127,7 @@ class TestDispersionBasedGazeMovementIdentifierClass(unittest.TestCase): ts_gaze_positions = ts_gaze_positions_A.append(ts_gaze_positions_B) - gaze_movement_identifier = DispersionBasedGazeMovementIdentifier.GazeMovementIdentifier(deviation_max_threshold=deviation_max, duration_min_threshold=max_time*2) + gaze_movement_identifier = DispersionThresholdIdentification.GazeMovementIdentifier(deviation_max_threshold=deviation_max, duration_min_threshold=max_time*2) ts_fixations, ts_saccades, ts_status = gaze_movement_identifier.browse(ts_gaze_positions) # Check result size @@ -159,7 +159,7 @@ class TestDispersionBasedGazeMovementIdentifierClass(unittest.TestCase): self.assertLessEqual(fixation.duration, size * max_time) ''' def test_fixation_and_short_saccade_identification(self): - """Test DispersionBasedGazeMovementIdentifier fixation and saccade identification.""" + """Test DispersionThresholdIdentification fixation and saccade identification.""" size = 10 move = 2 @@ -179,7 +179,7 @@ class TestDispersionBasedGazeMovementIdentifierClass(unittest.TestCase): ts_gaze_positions = ts_gaze_positions_A.append(ts_move_positions).append(ts_gaze_positions_B) - gaze_movement_identifier = DispersionBasedGazeMovementIdentifier.GazeMovementIdentifier(deviation_max_threshold=deviation_max, duration_min_threshold=max_time*2) + gaze_movement_identifier = DispersionThresholdIdentification.GazeMovementIdentifier(deviation_max_threshold=deviation_max, duration_min_threshold=max_time*2) ts_fixations, ts_saccades, ts_status = gaze_movement_identifier.browse(ts_gaze_positions) # Check result size @@ -211,7 +211,7 @@ class TestDispersionBasedGazeMovementIdentifierClass(unittest.TestCase): self.assertLessEqual(fixation.duration, size * max_time) ''' def test_invalid_gaze_position(self): - """Test DispersionBasedGazeMovementIdentifier fixation and saccade identification with invalid gaze position.""" + """Test DispersionThresholdIdentification fixation and saccade identification with invalid gaze position.""" size = 15 center = (0, 0) @@ -223,7 +223,7 @@ class TestDispersionBasedGazeMovementIdentifierClass(unittest.TestCase): ts_gaze_positions = build_gaze_fixation(size, center, deviation_max, start_time, min_time, max_time, validity) - gaze_movement_identifier = DispersionBasedGazeMovementIdentifier.GazeMovementIdentifier(deviation_max_threshold=deviation_max, duration_min_threshold=max_time*2) + gaze_movement_identifier = DispersionThresholdIdentification.GazeMovementIdentifier(deviation_max_threshold=deviation_max, duration_min_threshold=max_time*2) ts_fixations, ts_saccades, ts_status = gaze_movement_identifier.browse(ts_gaze_positions) # Check result size @@ -256,9 +256,9 @@ class TestDispersionBasedGazeMovementIdentifierClass(unittest.TestCase): ts_gaze_positions_B = build_gaze_fixation(size, center_B, deviation_max, start_time, min_time, max_time) ts_gaze_positions_C = build_gaze_fixation(size, center_C, deviation_max, start_time, min_time, max_time) - fixation_A = DispersionBasedGazeMovementIdentifier.Fixation(ts_gaze_positions_A) - fixation_B = DispersionBasedGazeMovementIdentifier.Fixation(ts_gaze_positions_B) - fixation_C = DispersionBasedGazeMovementIdentifier.Fixation(ts_gaze_positions_C) + fixation_A = DispersionThresholdIdentification.Fixation(ts_gaze_positions_A) + fixation_B = DispersionThresholdIdentification.Fixation(ts_gaze_positions_B) + fixation_C = DispersionThresholdIdentification.Fixation(ts_gaze_positions_C) # Check that fixation doesn't overlap self.assertFalse(fixation_A.overlap(fixation_B)) @@ -270,7 +270,7 @@ class TestDispersionBasedGazeMovementIdentifierClass(unittest.TestCase): @unittest.skip("Fixation overlapping identification is broken.") def test_fixation_overlapping_identification(self): - """Test DispersionBasedGazeMovementIdentifier identification when fixations overlap.""" + """Test DispersionThresholdIdentification identification when fixations overlap.""" size = 50 center_A = (-5, 0) @@ -285,7 +285,7 @@ class TestDispersionBasedGazeMovementIdentifierClass(unittest.TestCase): ts_gaze_positions = ts_gaze_positions_A.append(ts_gaze_positions_B) - gaze_movement_identifier = DispersionBasedGazeMovementIdentifier.GazeMovementIdentifier(deviation_max_threshold=deviation_max, duration_min_threshold=max_time*2) + gaze_movement_identifier = DispersionThresholdIdentification.GazeMovementIdentifier(deviation_max_threshold=deviation_max, duration_min_threshold=max_time*2) ts_fixations, ts_saccades, ts_status = gaze_movement_identifier.browse(ts_gaze_positions) # Check result size diff --git a/src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py b/src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py deleted file mode 100644 index cb29055..0000000 --- a/src/argaze/GazeAnalysis/DispersionBasedGazeMovementIdentifier.py +++ /dev/null @@ -1,189 +0,0 @@ -#!/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.""" - - 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.""" - - def __post_init__(self): - - super().__post_init__() - - self.update() - - 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 update(self): - """Update fixation's centroïd then maximal gaze positions deviation from this centroïd.""" - - 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 centroid attribute - object.__setattr__(self, 'centroid', (centroid_array[0], centroid_array[1])) - - # Update frozen deviation_max attribute - object.__setattr__(self, 'deviation_max', max(deviations_array)) - - 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-DT 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) - """ - - deviation_max_threshold: int|float - """Maximal distance 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.__valid_positions = GazeFeatures.TimeStampedGazePositions() - 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 - - # Check if too much time elapsed since last gaze position - if len(self.__valid_positions) > 0: - - ts_last, _ = self.__valid_positions.last - - if (ts - ts_last) > self.duration_min_threshold: - - # Clear all former gaze positions - self.__valid_positions = GazeFeatures.TimeStampedGazePositions() - self.__fixation_positions = GazeFeatures.TimeStampedGazePositions() - self.__saccade_positions = GazeFeatures.TimeStampedGazePositions() - - # Store gaze positions until a minimal duration - self.__valid_positions[ts] = gaze_position - - first_ts, _ = self.__valid_positions.first - last_ts, _ = self.__valid_positions.last - - # Once the minimal duration is reached - if last_ts - first_ts >= self.duration_min_threshold: - - # Calculate the deviation of valid gaze positions - deviation = Fixation(self.__valid_positions).deviation_max - - # Valid gaze positions deviation small enough - if deviation <= self.deviation_max_threshold: - - # Store last saccade - last_saccade = self.current_saccade - - # Clear saccade positions - self.__saccade_positions = GazeFeatures.TimeStampedGazePositions() - - # Copy valid gaze positions into fixation positions - self.__fixation_positions = self.__valid_positions.copy() - - # Output last saccade - return last_saccade if not terminate else self.current_fixation - - # Valid gaze positions deviation too wide while identifying fixation - elif len(self.__fixation_positions) > 0: - - # Store last fixation - last_fixation = self.current_fixation - - # Start saccade positions with current gaze position - self.__saccade_positions[ts] = gaze_position - - # Clear fixation positions - self.__fixation_positions = GazeFeatures.TimeStampedGazePositions() - - # Clear valid positions - self.__valid_positions = GazeFeatures.TimeStampedGazePositions() - - # Output last fixation - return last_fixation if not terminate else self.current_saccade - - # Valid gaze positions deviation too wide while identifying saccade (or not) - else: - - # Move oldest valid position into saccade positions - first_ts, first_position = self.__valid_positions.pop_first() - self.__saccade_positions[first_ts] = first_position - - @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) |