From e35e90d161ffb9202459631a0049448cde905b3c Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Wed, 31 May 2023 15:35:28 +0200 Subject: Testing all gaze analysis algorithm. Unifying paper citation. --- src/argaze.test/GazeAnalysis/Entropy.py | 12 ++-- src/argaze.test/GazeAnalysis/KCoefficient.py | 56 +++++++++++++++++ .../GazeAnalysis/LempelZivComplexity.py | 54 ++++++++++++++++ src/argaze.test/GazeAnalysis/NGram.py | 57 +++++++++++++++++ .../GazeAnalysis/NearestNeighborIndex.py | 40 ++++++++++++ src/argaze.test/GazeAnalysis/TransitionMatrix.py | 13 ++-- src/argaze.test/GazeFeatures.py | 72 ++++++++++++++-------- .../DispersionThresholdIdentification.py | 13 ++-- src/argaze/GazeAnalysis/Entropy.py | 12 ++-- src/argaze/GazeAnalysis/KCoefficient.py | 25 +++++--- src/argaze/GazeAnalysis/LempelZivComplexity.py | 13 ++-- src/argaze/GazeAnalysis/NGram.py | 12 ++-- src/argaze/GazeAnalysis/NearestNeighborIndex.py | 8 ++- src/argaze/GazeAnalysis/README.md | 2 +- src/argaze/GazeAnalysis/TransitionMatrix.py | 8 +-- .../VelocityThresholdIdentification.py | 13 ++-- 16 files changed, 336 insertions(+), 74 deletions(-) create mode 100644 src/argaze.test/GazeAnalysis/KCoefficient.py create mode 100644 src/argaze.test/GazeAnalysis/LempelZivComplexity.py create mode 100644 src/argaze.test/GazeAnalysis/NGram.py create mode 100644 src/argaze.test/GazeAnalysis/NearestNeighborIndex.py (limited to 'src') diff --git a/src/argaze.test/GazeAnalysis/Entropy.py b/src/argaze.test/GazeAnalysis/Entropy.py index 47d5556..b69f329 100644 --- a/src/argaze.test/GazeAnalysis/Entropy.py +++ b/src/argaze.test/GazeAnalysis/Entropy.py @@ -18,20 +18,20 @@ GazeFeaturesTest = MiscFeatures.importFromTestPackage('GazeFeatures') class TestAOIScanPathAnalyzer(unittest.TestCase): """Test AOIScanPathAnalyzer class.""" - def test_analyse(self): - """Test analyse method.""" - - aoi_scan_path = GazeFeaturesTest.build_aoi_scan_path(['Foo', 'Bar', 'Shu'], ['Bar', 'Shu', 'Foo', 'Bar', 'Shu', 'Foo', 'Bar', 'Shu', 'Foo']) + def test_analyze(self): + """Test analyze method.""" entropy_analyzer = Entropy.AOIScanPathAnalyzer() transition_matrix_analyser = TransitionMatrix.AOIScanPathAnalyzer() - transition_matrix_probabilities, transition_matrix_density = transition_matrix_analyser.analyze(aoi_scan_path) - stationary_entropy, transition_entropy = entropy_analyzer.analyze(aoi_scan_path, transition_matrix_probabilities) + aoi_scan_path = GazeFeaturesTest.build_aoi_scan_path(['Foo', 'Bar', 'Shu'], ['Bar', 'Shu', 'Foo', 'Bar', 'Shu', 'Foo', 'Bar', 'Shu', 'Foo']) # Check aoi scan path self.assertEqual(len(aoi_scan_path), 9) + transition_matrix_probabilities, transition_matrix_density = transition_matrix_analyser.analyze(aoi_scan_path) + stationary_entropy, transition_entropy = entropy_analyzer.analyze(aoi_scan_path, transition_matrix_probabilities) + # Check entropy analysis self.assertAlmostEqual(stationary_entropy, 1.09, 1) self.assertAlmostEqual(transition_entropy, 0, 1) diff --git a/src/argaze.test/GazeAnalysis/KCoefficient.py b/src/argaze.test/GazeAnalysis/KCoefficient.py new file mode 100644 index 0000000..07dff79 --- /dev/null +++ b/src/argaze.test/GazeAnalysis/KCoefficient.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python + +""" """ + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "BSD" + +import unittest + +from argaze import GazeFeatures +from argaze.GazeAnalysis import KCoefficient +from argaze.utils import MiscFeatures + +GazeFeaturesTest = MiscFeatures.importFromTestPackage('GazeFeatures') + +class TestScanPathAnalyzer(unittest.TestCase): + """Test ScanPathAnalyzer class.""" + + def test_analyze(self): + """Test analyze method.""" + + kcoeff_analyzer = KCoefficient.AOIScanPathAnalyzer() + + scan_path = GazeFeaturesTest.build_scan_path(10) + + # Check scan path + self.assertEqual(len(scan_path), 10) + + K = kcoeff_analyzer.analyze(scan_path) + + # Check that K coefficient is almost equal to 0 + self.assertAlmostEqual(K, 0) + +class TestAOIScanPathAnalyzer(unittest.TestCase): + """Test AOIScanPathAnalyzer class.""" + + def test_analyze(self): + """Test analyze method.""" + + kcoeff_analyzer = KCoefficient.AOIScanPathAnalyzer() + + aoi_scan_path = GazeFeaturesTest.build_aoi_scan_path(['Foo', 'Bar', 'Shu'], ['Bar', 'Shu', 'Foo', 'Bar']) + + # Check aoi scan path + self.assertEqual(len(aoi_scan_path), 4) + + K = kcoeff_analyzer.analyze(aoi_scan_path) + + # Check that K coefficient is almost equal to 0 + self.assertAlmostEqual(K, 0) + +if __name__ == '__main__': + + unittest.main() \ No newline at end of file diff --git a/src/argaze.test/GazeAnalysis/LempelZivComplexity.py b/src/argaze.test/GazeAnalysis/LempelZivComplexity.py new file mode 100644 index 0000000..75afc4d --- /dev/null +++ b/src/argaze.test/GazeAnalysis/LempelZivComplexity.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python + +""" """ + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "BSD" + +import unittest + +from argaze import GazeFeatures +from argaze.GazeAnalysis import LempelZivComplexity +from argaze.utils import MiscFeatures + +GazeFeaturesTest = MiscFeatures.importFromTestPackage('GazeFeatures') + +class TestAOIScanPathAnalyzer(unittest.TestCase): + """Test AOIScanPathAnalyzer class.""" + + @unittest.skip("The result is not like in the paper.") + def test_analyze_first_example(self): + """Test analyze method with first example sequence from the paper.""" + + lzc_analyzer = LempelZivComplexity.AOIScanPathAnalyzer() + + aoi_scan_path = GazeFeaturesTest.build_aoi_scan_path(['Foo', 'Bar', 'Shu'], ['Bar', 'Shu', 'Bar', 'Shu', 'Bar', 'Shu', 'Bar', 'Shu', 'Bar', 'Shu', 'Bar', 'Shu', 'Foo']) + + # Check aoi scan path + self.assertEqual(len(aoi_scan_path), 13) + + lzc = lzc_analyzer.analyze(aoi_scan_path) + + # Check LZC coefficient + self.assertEqual(lzc, 6) + + def test_analyze_seconde_example(self): + """Test analyze method with second example sequence from the paper.""" + + lzc_analyzer = LempelZivComplexity.AOIScanPathAnalyzer() + + aoi_scan_path = GazeFeaturesTest.build_aoi_scan_path(['Ade', 'Bar', 'Cob', 'Gno', 'Kel', 'Iop', 'Eca'], ['Bar', 'Cob', 'Ade', 'Kel', 'Ade', 'Cob', 'Kel', 'Gno', 'Kel', 'Ade', 'Kel', 'Iop', 'Eca']) + + # Check aoi scan path + self.assertEqual(len(aoi_scan_path), 13) + + lzc = lzc_analyzer.analyze(aoi_scan_path) + + # Check LZC coefficient + self.assertEqual(lzc, 9) + +if __name__ == '__main__': + + unittest.main() \ No newline at end of file diff --git a/src/argaze.test/GazeAnalysis/NGram.py b/src/argaze.test/GazeAnalysis/NGram.py new file mode 100644 index 0000000..9608b90 --- /dev/null +++ b/src/argaze.test/GazeAnalysis/NGram.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python + +""" """ + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "BSD" + +import unittest + +from argaze import GazeFeatures +from argaze.GazeAnalysis import NGram +from argaze.utils import MiscFeatures + +GazeFeaturesTest = MiscFeatures.importFromTestPackage('GazeFeatures') + +class TestAOIScanPathAnalyzer(unittest.TestCase): + """Test AOIScanPathAnalyzer class.""" + + def test_analyze(self): + """Test analyze method.""" + + ngram_analyzer = NGram.AOIScanPathAnalyzer() + + aoi_scan_path = GazeFeaturesTest.build_aoi_scan_path(['Foo', 'Bar', 'Shu'], ['Bar', 'Shu', 'Foo', 'Bar', 'Shu', 'Foo']) + + # Check aoi scan path + self.assertEqual(len(aoi_scan_path), 6) + + ngram_analysis = ngram_analyzer.analyze(aoi_scan_path, 2) + + # Check 2-gram analysis + self.assertEqual(len(ngram_analysis), 3) + self.assertEqual(ngram_analysis[('Bar', 'Shu')], 2) + self.assertEqual(ngram_analysis[('Shu', 'Foo')], 2) + self.assertEqual(ngram_analysis[('Foo', 'Bar')], 1) + + ngram_analysis = ngram_analyzer.analyze(aoi_scan_path, 3) + + # Check 3-gram analysis + self.assertEqual(len(ngram_analysis), 3) + self.assertEqual(ngram_analysis[('Bar', 'Shu', 'Foo')], 2) + self.assertEqual(ngram_analysis[('Shu', 'Foo', 'Bar')], 1) + self.assertEqual(ngram_analysis[('Foo', 'Bar', 'Shu')], 1) + + ngram_analysis = ngram_analyzer.analyze(aoi_scan_path, 4) + + # Check 4-gram analysis + self.assertEqual(len(ngram_analysis), 3) + self.assertEqual(ngram_analysis[('Bar', 'Shu', 'Foo', 'Bar')], 1) + self.assertEqual(ngram_analysis[('Shu', 'Foo', 'Bar', 'Shu')], 1) + self.assertEqual(ngram_analysis[('Foo', 'Bar', 'Shu', 'Foo')], 1) + +if __name__ == '__main__': + + unittest.main() \ No newline at end of file diff --git a/src/argaze.test/GazeAnalysis/NearestNeighborIndex.py b/src/argaze.test/GazeAnalysis/NearestNeighborIndex.py new file mode 100644 index 0000000..fb7d4ec --- /dev/null +++ b/src/argaze.test/GazeAnalysis/NearestNeighborIndex.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + +""" """ + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "BSD" + +import unittest + +from argaze import GazeFeatures +from argaze.GazeAnalysis import NearestNeighborIndex +from argaze.utils import MiscFeatures + +GazeFeaturesTest = MiscFeatures.importFromTestPackage('GazeFeatures') + +class TestScanPathAnalyzer(unittest.TestCase): + """Test ScanPathAnalyzer class.""" + + def test_analyze(self): + """Test analyze.""" + + nni_analyzer = NearestNeighborIndex.ScanPathAnalyzer() + + screen_dimension = (100, 100) + scan_path = GazeFeaturesTest.build_scan_path(6, screen_dimension) + + # Check aoi scan path + self.assertEqual(len(scan_path), 6) + + nni = nni_analyzer.analyze(scan_path, screen_dimension) + + # Check NNI + self.assertGreaterEqual(nni, 0) + self.assertLessEqual(nni, 1) + +if __name__ == '__main__': + + unittest.main() \ No newline at end of file diff --git a/src/argaze.test/GazeAnalysis/TransitionMatrix.py b/src/argaze.test/GazeAnalysis/TransitionMatrix.py index 14a34ce..997b706 100644 --- a/src/argaze.test/GazeAnalysis/TransitionMatrix.py +++ b/src/argaze.test/GazeAnalysis/TransitionMatrix.py @@ -18,17 +18,18 @@ GazeFeaturesTest = MiscFeatures.importFromTestPackage('GazeFeatures') class TestAOIScanPathAnalyzer(unittest.TestCase): """Test AOIScanPathAnalyzer class.""" - def test_analyse(self): - """Test analyse method.""" - - aoi_scan_path = GazeFeaturesTest.build_aoi_scan_path(['Foo', 'Bar', 'Shu'], ['Bar', 'Shu', 'Foo', 'Bar']) - + def test_analyze(self): + """Test analyze method.""" + transition_matrix_analyser = TransitionMatrix.AOIScanPathAnalyzer() - transition_matrix_probabilities, transition_matrix_density = transition_matrix_analyser.analyze(aoi_scan_path) + + aoi_scan_path = GazeFeaturesTest.build_aoi_scan_path(['Foo', 'Bar', 'Shu'], ['Bar', 'Shu', 'Foo', 'Bar']) # Check aoi scan path self.assertEqual(len(aoi_scan_path), 4) + transition_matrix_probabilities, transition_matrix_density = transition_matrix_analyser.analyze(aoi_scan_path) + # Check transition matrix probabilities ([destination][departure]) self.assertEqual(transition_matrix_probabilities['Foo']['Foo'], 0) self.assertEqual(transition_matrix_probabilities['Bar']['Bar'], 0) diff --git a/src/argaze.test/GazeFeatures.py b/src/argaze.test/GazeFeatures.py index 8206baf..f73eefe 100644 --- a/src/argaze.test/GazeFeatures.py +++ b/src/argaze.test/GazeFeatures.py @@ -8,12 +8,13 @@ __copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" __license__ = "BSD" import unittest +from dataclasses import dataclass from argaze import GazeFeatures import numpy -def random_gaze_positions(size): +def random_gaze_positions(size, screen_dimension: tuple[float, float] = (1, 1)): """ Generate random TimeStampedGazePsoitions for testing purpose. Timestamps are current time. GazePositions are random values. @@ -27,13 +28,36 @@ def random_gaze_positions(size): for i in range(0, size): # Edit gaze position - random_gaze_position = GazeFeatures.GazePosition((random.random(), random.random())) + random_gaze_position = GazeFeatures.GazePosition((random.random() * screen_dimension[0], random.random() * screen_dimension[1])) # Store gaze position ts_gaze_positions[time.time()] = random_gaze_position return ts_gaze_positions +@dataclass(frozen=True) +class TestFixation(GazeFeatures.Fixation): + """Define basic fixation class for test.""" + + 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)]) + + # Update frozen focus attribute using centroid + object.__setattr__(self, 'focus', (centroid_array[0], centroid_array[1])) + +@dataclass(frozen=True) +class TestSaccade(GazeFeatures.Saccade): + """Define basic saccade for test.""" + + def __post_init__(self): + super().__post_init__() + class TestGazePositionClass(unittest.TestCase): """Test GazePosition class.""" @@ -222,8 +246,8 @@ class TestScanStepClass(unittest.TestCase): def test_new(self): """Test ScanStep creation.""" - fixation = GazeFeatures.Fixation(random_gaze_positions(10)) - saccade = GazeFeatures.Saccade(random_gaze_positions(2)) + fixation = TestFixation(random_gaze_positions(10)) + saccade = TestSaccade(random_gaze_positions(2)) scan_step = GazeFeatures.ScanStep(fixation, saccade) @@ -232,18 +256,18 @@ class TestScanStepClass(unittest.TestCase): self.assertEqual(scan_step.last_saccade, saccade) self.assertGreater(scan_step.duration, 0) -def build_scan_path(size): +def build_scan_path(size, screen_dimension: tuple[float, float] = (1, 1)): """Build scan path""" scan_path = GazeFeatures.ScanPath() for i in range(size): - fixation = GazeFeatures.Fixation(random_gaze_positions(10)) + fixation = TestFixation(random_gaze_positions(10, screen_dimension)) ts, _ = fixation.positions.first scan_path.append_fixation(ts, fixation) - saccade = GazeFeatures.Saccade(random_gaze_positions(2)) + saccade = TestSaccade(random_gaze_positions(2, screen_dimension)) ts, _ = saccade.positions.first scan_path.append_saccade(ts, saccade) @@ -266,7 +290,7 @@ class TestScanPathClass(unittest.TestCase): scan_path = GazeFeatures.ScanPath() # Append a saccade that should be ignored - saccade = GazeFeatures.Saccade(random_gaze_positions(2)) + saccade = TestSaccade(random_gaze_positions(2)) ts, _ = saccade.positions.first new_step = scan_path.append_saccade(ts, saccade) @@ -276,7 +300,7 @@ class TestScanPathClass(unittest.TestCase): self.assertEqual(new_step, None) # Append first fixation - fixation_A = GazeFeatures.Fixation(random_gaze_positions(10)) + fixation_A = TestFixation(random_gaze_positions(10)) ts, _ = fixation_A.positions.first new_step = scan_path.append_fixation(ts, fixation_A) @@ -286,7 +310,7 @@ class TestScanPathClass(unittest.TestCase): self.assertEqual(new_step, None) # Append consecutive saccade - saccade_A = GazeFeatures.Saccade(random_gaze_positions(2)) + saccade_A = TestSaccade(random_gaze_positions(2)) ts, _ = saccade_A.positions.first new_step = scan_path.append_saccade(ts, saccade_A) @@ -297,7 +321,7 @@ class TestScanPathClass(unittest.TestCase): self.assertEqual(new_step.last_saccade, saccade_A) # Append 2 consecutive fixations then a saccade - fixation_B1 = GazeFeatures.Fixation(random_gaze_positions(10)) + fixation_B1 = TestFixation(random_gaze_positions(10)) ts, _ = fixation_B1.positions.first new_step = scan_path.append_fixation(ts, fixation_B1) @@ -306,7 +330,7 @@ class TestScanPathClass(unittest.TestCase): self.assertEqual(len(scan_path), 1) self.assertEqual(new_step, None) - fixation_B2 = GazeFeatures.Fixation(random_gaze_positions(10)) + fixation_B2 = TestFixation(random_gaze_positions(10)) ts, _ = fixation_B2.positions.first new_step = scan_path.append_fixation(ts, fixation_B2) @@ -315,7 +339,7 @@ class TestScanPathClass(unittest.TestCase): self.assertEqual(len(scan_path), 1) self.assertEqual(new_step, None) - saccade_B = GazeFeatures.Saccade(random_gaze_positions(2)) + saccade_B = TestSaccade(random_gaze_positions(2)) ts, _ = saccade_B.positions.first new_step = scan_path.append_saccade(ts, saccade_B) @@ -333,11 +357,11 @@ class TestAOIScanStepClass(unittest.TestCase): movements = GazeFeatures.TimeStampedGazeMovements() - fixation = GazeFeatures.Fixation(random_gaze_positions(10)) + fixation = TestFixation(random_gaze_positions(10)) ts, _ = fixation.positions.first movements[ts] = fixation - saccade = GazeFeatures.Saccade(random_gaze_positions(2)) + saccade = TestSaccade(random_gaze_positions(2)) ts, _ = saccade.positions.first movements[ts] = saccade @@ -355,11 +379,11 @@ class TestAOIScanStepClass(unittest.TestCase): movements = GazeFeatures.TimeStampedGazeMovements() - saccade = GazeFeatures.Saccade(random_gaze_positions(2)) + saccade = TestSaccade(random_gaze_positions(2)) ts, _ = saccade.positions.first movements[ts] = saccade - fixation = GazeFeatures.Fixation(random_gaze_positions(10)) + fixation = TestFixation(random_gaze_positions(10)) ts, _ = fixation.positions.first movements[ts] = fixation @@ -378,11 +402,11 @@ def build_aoi_scan_path(expected_aois, aoi_path): for aoi in aoi_path: - fixation = GazeFeatures.Fixation(random_gaze_positions(10)) + fixation = TestFixation(random_gaze_positions(10)) ts, _ = fixation.positions.first aoi_scan_path.append_fixation(ts, fixation, aoi) - saccade = GazeFeatures.Saccade(random_gaze_positions(2)) + saccade = TestSaccade(random_gaze_positions(2)) ts, _ = saccade.positions.first aoi_scan_path.append_saccade(ts, saccade) @@ -405,7 +429,7 @@ class TestAOIScanPathClass(unittest.TestCase): aoi_scan_path = GazeFeatures.AOIScanPath(['Foo', 'Bar']) # Append fixation on A aoi - fixation = GazeFeatures.Fixation(random_gaze_positions(10)) + fixation = TestFixation(random_gaze_positions(10)) ts, _ = fixation.positions.first new_step = aoi_scan_path.append_fixation(ts, fixation, 'Foo') @@ -415,7 +439,7 @@ class TestAOIScanPathClass(unittest.TestCase): self.assertEqual(new_step, None) # Append saccade - saccade = GazeFeatures.Saccade(random_gaze_positions(2)) + saccade = TestSaccade(random_gaze_positions(2)) ts, _ = saccade.positions.first new_step = aoi_scan_path.append_saccade(ts, saccade) @@ -425,7 +449,7 @@ class TestAOIScanPathClass(unittest.TestCase): self.assertEqual(new_step, None) # Append fixation on B aoi - fixation = GazeFeatures.Fixation(random_gaze_positions(10)) + fixation = TestFixation(random_gaze_positions(10)) ts, _ = fixation.positions.first new_step = aoi_scan_path.append_fixation(ts, fixation, 'Bar') @@ -445,7 +469,7 @@ class TestAOIScanPathClass(unittest.TestCase): aoi_scan_path = GazeFeatures.AOIScanPath(['Foo', 'Bar']) # Append fixation on A aoi - fixation = GazeFeatures.Fixation(random_gaze_positions(10)) + fixation = TestFixation(random_gaze_positions(10)) ts, _ = fixation.positions.first new_step = aoi_scan_path.append_fixation(ts, fixation, 'Foo') @@ -455,7 +479,7 @@ class TestAOIScanPathClass(unittest.TestCase): self.assertEqual(new_step, None) # Append fixation on B aoi - fixation = GazeFeatures.Fixation(random_gaze_positions(10)) + fixation = TestFixation(random_gaze_positions(10)) ts, _ = fixation.positions.first # Check that aoi scan step creation fail when fixation is appened after another fixation diff --git a/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py b/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py index 48ada31..c6353d4 100644 --- a/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py +++ b/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py @@ -2,10 +2,10 @@ """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, 71-78. - [DOI=http://dx.doi.org/10.1145/355017.355028](DOI=http://dx.doi.org/10.1145/355017.355028) + **Dario D. Salvucci and Joseph H. Goldberg (2000).** + *Identifying fixations and saccades in eye-tracking protocols.* + Proceedings of the 2000 symposium on Eye tracking research & applications (ETRA'00, 71-78). + [https://doi.org/10.1145/355017.355028](https://doi.org/10.1145/355017.355028) """ __author__ = "Théo de la Hogue" @@ -22,6 +22,9 @@ from argaze import GazeFeatures import numpy import cv2 +GazeMovementType = TypeVar('GazeMovement', bound="GazeMovement") +# Type definition for type annotation convenience + FixationType = TypeVar('Fixation', bound="Fixation") # Type definition for type annotation convenience @@ -112,7 +115,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): self.__fixation_positions = GazeFeatures.TimeStampedGazePositions() self.__saccade_positions = GazeFeatures.TimeStampedGazePositions() - def identify(self, ts, gaze_position, terminate=False): + def identify(self, ts, gaze_position, terminate=False) -> GazeMovementType: """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. diff --git a/src/argaze/GazeAnalysis/Entropy.py b/src/argaze/GazeAnalysis/Entropy.py index a760b32..05ac5ea 100644 --- a/src/argaze/GazeAnalysis/Entropy.py +++ b/src/argaze/GazeAnalysis/Entropy.py @@ -2,10 +2,10 @@ """Implementation of entropy algorithm as described in: - K Krejtz, T Szmidt, AT Duchowski. 2014. - Entropy-based statistical analysis of eye movement transitions. - In Proceedings of the Symposium on Eye Tracking Research and Applications, ETRA '14, 159-166. - [DOI=https://doi.org/10.1145/2578153.2578176](DOI=https://doi.org/10.1145/2578153.2578176) + **Krejtz K., Szmidt T., Duchowski A.T. (2014).** + *Entropy-based statistical analysis of eye movement transitions.* + Proceedings of the Symposium on Eye Tracking Research and Applications (ETRA'14, 159-166). + [https://doi.org/10.1145/2578153.2578176](https://doi.org/10.1145/2578153.2578176) """ __author__ = "Théo de la Hogue" @@ -13,8 +13,8 @@ __credits__ = [] __copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" __license__ = "BSD" -from typing import TypeVar, Tuple, Any -from dataclasses import dataclass, field +from typing import Tuple +from dataclasses import dataclass from argaze import GazeFeatures diff --git a/src/argaze/GazeAnalysis/KCoefficient.py b/src/argaze/GazeAnalysis/KCoefficient.py index 0bc4395..5768d1b 100644 --- a/src/argaze/GazeAnalysis/KCoefficient.py +++ b/src/argaze/GazeAnalysis/KCoefficient.py @@ -1,15 +1,14 @@ #!/usr/bin/env python -""" """ +"""Implementation of K coefficient and K-modified coefficient. +""" __author__ = "Théo de la Hogue" __credits__ = [] __copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" __license__ = "BSD" -from typing import TypeVar, Tuple, Any -from dataclasses import dataclass, field -import math +from dataclasses import dataclass from argaze import GazeFeatures @@ -17,14 +16,19 @@ import numpy @dataclass class ScanPathAnalyzer(GazeFeatures.ScanPathAnalyzer): - """Implementation of Coefficient K algorithm as proposed by A. Duchowski and Krejtz, 2017. + """Implementation of the K coefficient algorithm as described in: + + **Krejtz K., Duchowski A., Krejtz I., Szarkowska A., & Kopacz A. (2016).** + *Discerning ambient/focal attention with coefficient K.* + ACM Transactions on Applied Perception (TAP, 1–20). + [https://doi.org/10.1145/2896452](https://doi.org/10.1145/2896452) """ def __post_init__(self): pass - def analyze(self, scan_path: GazeFeatures.ScanPathType) -> Any: + def analyze(self, scan_path: GazeFeatures.ScanPathType) -> float: """Analyze scan path.""" assert(len(scan_path) > 1) @@ -57,14 +61,19 @@ class ScanPathAnalyzer(GazeFeatures.ScanPathAnalyzer): @dataclass class AOIScanPathAnalyzer(GazeFeatures.AOIScanPathAnalyzer): - """Implementation of AOI based Coefficient K algorithm as described by Christophe Lounis in its thesis "Monitor the monitoring: pilot assistance through gaze tracking and aoi scanning analyses". + """Implementation of the K-modified coefficient algorithm as described in: + + **Lounis, C. A., Hassoumi, A., Lefrancois, O., Peysakhovich, V., & Causse, M. (2020, June).** + *Detecting ambient/focal visual attention in professional airline pilots with a modified Coefficient K: a full flight simulator study.* + ACM Symposium on Eye Tracking Research and Applications (ETRA'20, 1-6). + [https://doi.org/10.1145/3379157.3391412](https://doi.org/10.1145/3379157.3391412) """ def __post_init__(self): pass - def analyze(self, aoi_scan_path: GazeFeatures.AOIScanPathType) -> Any: + def analyze(self, aoi_scan_path: GazeFeatures.AOIScanPathType) -> float: """Analyze aoi scan path.""" assert(len(aoi_scan_path) > 1) diff --git a/src/argaze/GazeAnalysis/LempelZivComplexity.py b/src/argaze/GazeAnalysis/LempelZivComplexity.py index 47999f5..ee73820 100644 --- a/src/argaze/GazeAnalysis/LempelZivComplexity.py +++ b/src/argaze/GazeAnalysis/LempelZivComplexity.py @@ -1,14 +1,19 @@ #!/usr/bin/env python -""" """ +"""Implementation of Lempel-Ziv complexity algorithm as described in: + + **Lounis C., Peysakhovich V., Causse M. (2020).** + *Lempel-Ziv Complexity of dwell sequences: visual scanning pattern differences between novice and expert aircraft pilots.* + Proceedings of the 1st International Workshop on Eye-Tracking in Aviation (ETAVI'20, 61-68). + [https://doi.org/10.3929/ethz-b-000407653](https://doi.org/10.3929/ethz-b-000407653) +""" __author__ = "Théo de la Hogue" __credits__ = [] __copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" __license__ = "BSD" -from typing import TypeVar, Tuple, Any -from dataclasses import dataclass, field +from dataclasses import dataclass from argaze import GazeFeatures @@ -23,7 +28,7 @@ class AOIScanPathAnalyzer(GazeFeatures.AOIScanPathAnalyzer): pass - def analyze(self, aoi_scan_path: GazeFeatures.AOIScanPathType) -> Any: + def analyze(self, aoi_scan_path: GazeFeatures.AOIScanPathType) -> int: """Analyze aoi scan path.""" assert(len(aoi_scan_path) > 1) diff --git a/src/argaze/GazeAnalysis/NGram.py b/src/argaze/GazeAnalysis/NGram.py index c3ff337..f3f0cca 100644 --- a/src/argaze/GazeAnalysis/NGram.py +++ b/src/argaze/GazeAnalysis/NGram.py @@ -1,6 +1,12 @@ #!/usr/bin/env python -""" """ +"""Implementation of N-Gram algorithm as proposed in: + + **Lounis C., Peysakhovich V., Causse M. (2021).** + *Visual scanning strategies in the cockpit are modulated by pilots’ expertise: A flight simulator study.* + PLoS ONE (16(2), 6). + [https://doi.org/10.1371/journal.pone.0247061](https://doi.org/10.1371/journal.pone.0247061) +""" __author__ = "Théo de la Hogue" __credits__ = [] @@ -14,14 +20,12 @@ from argaze import GazeFeatures @dataclass class AOIScanPathAnalyzer(GazeFeatures.AOIScanPathAnalyzer): - """Implementation of N-gram algorithm as ... - """ def __post_init__(self): pass - def analyze(self, aoi_scan_path: GazeFeatures.AOIScanPathType, n: int) -> list: + def analyze(self, aoi_scan_path: GazeFeatures.AOIScanPathType, n: int) -> dict: """Analyze aoi scan path.""" assert(len(aoi_scan_path) > 1) diff --git a/src/argaze/GazeAnalysis/NearestNeighborIndex.py b/src/argaze/GazeAnalysis/NearestNeighborIndex.py index 104bb30..b9654de 100644 --- a/src/argaze/GazeAnalysis/NearestNeighborIndex.py +++ b/src/argaze/GazeAnalysis/NearestNeighborIndex.py @@ -1,6 +1,12 @@ #!/usr/bin/env python -""" """ +"""Implementation of Nearest Neighbor Index algorithm as described in: + + **Di Nocera F., Terenzi M., Camilli M. (2006).** + *Another look at scanpath: distance to nearest neighbour as a measure of mental workload.* + Developments in Human Factors in Transportation, Design, and Evaluation. + [https://www.researchgate.net](https://www.researchgate.net/publication/239470608_Another_look_at_scanpath_distance_to_nearest_neighbour_as_a_measure_of_mental_workload) +""" __author__ = "Théo de la Hogue" __credits__ = [] diff --git a/src/argaze/GazeAnalysis/README.md b/src/argaze/GazeAnalysis/README.md index 3084c15..b2582cb 100644 --- a/src/argaze/GazeAnalysis/README.md +++ b/src/argaze/GazeAnalysis/README.md @@ -1,4 +1,4 @@ -Class interface to work with various gaze analysis algorithms. +Various gaze movement identification and scan path analysis algorithms. ## Wiki diff --git a/src/argaze/GazeAnalysis/TransitionMatrix.py b/src/argaze/GazeAnalysis/TransitionMatrix.py index c413072..e92baf3 100644 --- a/src/argaze/GazeAnalysis/TransitionMatrix.py +++ b/src/argaze/GazeAnalysis/TransitionMatrix.py @@ -2,10 +2,10 @@ """Implementation of transition matrix probabilities and density algorithm as described in: - K Krejtz, T Szmidt, AT Duchowski. 2014. - Entropy-based statistical analysis of eye movement transitions. - In Proceedings of the Symposium on Eye Tracking Research and Applications, ETRA '14, 159-166. - [DOI=https://doi.org/10.1145/2578153.2578176](DOI=https://doi.org/10.1145/2578153.2578176) + **Krejtz K., Szmidt T., Duchowski A.T. (2014).** + *Entropy-based statistical analysis of eye movement transitions.* + Proceedings of the Symposium on Eye Tracking Research and Applications, (ETRA'14, 159-166). + [https://doi.org/10.1145/2578153.2578176](https://doi.org/10.1145/2578153.2578176) """ __author__ = "Théo de la Hogue" diff --git a/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py b/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py index e819522..4c97c4c 100644 --- a/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py +++ b/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py @@ -2,10 +2,10 @@ """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, 71-78. - [DOI=http://dx.doi.org/10.1145/355017.355028](DOI=http://dx.doi.org/10.1145/355017.355028) + **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, 71-78). + [https://doi.org/10.1145/355017.355028](https://doi.org/10.1145/355017.355028) """ __author__ = "Théo de la Hogue" @@ -22,6 +22,9 @@ from argaze import GazeFeatures import numpy import cv2 +GazeMovementType = TypeVar('GazeMovement', bound="GazeMovement") +# Type definition for type annotation convenience + FixationType = TypeVar('Fixation', bound="Fixation") # Type definition for type annotation convenience @@ -121,7 +124,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): self.__fixation_positions = GazeFeatures.TimeStampedGazePositions() self.__saccade_positions = GazeFeatures.TimeStampedGazePositions() - def identify(self, ts, gaze_position, terminate=False): + def identify(self, ts, gaze_position, terminate=False) -> GazeMovementType: """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. -- cgit v1.1