aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThéo de la Hogue2023-05-31 15:35:28 +0200
committerThéo de la Hogue2023-05-31 15:35:28 +0200
commite35e90d161ffb9202459631a0049448cde905b3c (patch)
tree0ad987c3ffcea30f739cc8e106df2a5f00ad22da
parentd1cdae406d349023eb9e221fd226c05744064dfd (diff)
downloadargaze-e35e90d161ffb9202459631a0049448cde905b3c.zip
argaze-e35e90d161ffb9202459631a0049448cde905b3c.tar.gz
argaze-e35e90d161ffb9202459631a0049448cde905b3c.tar.bz2
argaze-e35e90d161ffb9202459631a0049448cde905b3c.tar.xz
Testing all gaze analysis algorithm. Unifying paper citation.
-rw-r--r--src/argaze.test/GazeAnalysis/Entropy.py12
-rw-r--r--src/argaze.test/GazeAnalysis/KCoefficient.py56
-rw-r--r--src/argaze.test/GazeAnalysis/LempelZivComplexity.py54
-rw-r--r--src/argaze.test/GazeAnalysis/NGram.py57
-rw-r--r--src/argaze.test/GazeAnalysis/NearestNeighborIndex.py40
-rw-r--r--src/argaze.test/GazeAnalysis/TransitionMatrix.py13
-rw-r--r--src/argaze.test/GazeFeatures.py72
-rw-r--r--src/argaze/GazeAnalysis/DispersionThresholdIdentification.py13
-rw-r--r--src/argaze/GazeAnalysis/Entropy.py12
-rw-r--r--src/argaze/GazeAnalysis/KCoefficient.py25
-rw-r--r--src/argaze/GazeAnalysis/LempelZivComplexity.py13
-rw-r--r--src/argaze/GazeAnalysis/NGram.py12
-rw-r--r--src/argaze/GazeAnalysis/NearestNeighborIndex.py8
-rw-r--r--src/argaze/GazeAnalysis/README.md2
-rw-r--r--src/argaze/GazeAnalysis/TransitionMatrix.py8
-rw-r--r--src/argaze/GazeAnalysis/VelocityThresholdIdentification.py13
16 files changed, 336 insertions, 74 deletions
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.