From 717452611747ab98a5e94c1f90afcc2854923c68 Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Thu, 6 Jul 2023 15:50:31 +0200 Subject: Adding and testing finished attribute to GazeMovement class. --- .../DispersionThresholdIdentification.py | 14 +++++++++- .../VelocityThresholdIdentification.py | 9 +++++++ src/argaze.test/GazeFeatures.py | 31 ++++++++++++++++++++++ .../DispersionThresholdIdentification.py | 11 +++++++- .../VelocityThresholdIdentification.py | 11 ++++++++ src/argaze/GazeFeatures.py | 9 +++++++ 6 files changed, 83 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/argaze.test/GazeAnalysis/DispersionThresholdIdentification.py b/src/argaze.test/GazeAnalysis/DispersionThresholdIdentification.py index eba80c8..f1d02d6 100644 --- a/src/argaze.test/GazeAnalysis/DispersionThresholdIdentification.py +++ b/src/argaze.test/GazeAnalysis/DispersionThresholdIdentification.py @@ -120,6 +120,7 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): self.assertLessEqual(fixation.deviation_max, deviation_max) self.assertGreaterEqual(fixation.duration, size * min_time) self.assertLessEqual(fixation.duration, size * max_time) + self.assertLessEqual(fixation.finished, True) def test_fixation_and_direct_saccade_identification(self): """Test DispersionThresholdIdentification fixation and saccade identification.""" @@ -151,6 +152,7 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): self.assertLessEqual(fixation.deviation_max, deviation_max) self.assertGreaterEqual(fixation.duration, size * min_time) self.assertLessEqual(fixation.duration, size * max_time) + self.assertLessEqual(fixation.finished, True) # Check first saccade ts, saccade = ts_saccades.pop_first() @@ -158,6 +160,7 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): self.assertEqual(len(saccade.positions.keys()), 1) self.assertGreaterEqual(saccade.duration, 0.) self.assertLessEqual(saccade.duration, 0.) + self.assertLessEqual(saccade.finished, True) # Check second fixation ts, fixation = ts_fixations.pop_first() @@ -166,6 +169,7 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): self.assertLessEqual(fixation.deviation_max, deviation_max) self.assertGreaterEqual(fixation.duration, size * min_time) self.assertLessEqual(fixation.duration, size * max_time) + self.assertLessEqual(fixation.finished, True) def test_fixation_and_short_saccade_identification(self): """Test DispersionThresholdIdentification fixation and saccade identification.""" @@ -200,6 +204,7 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): self.assertLessEqual(fixation.deviation_max, deviation_max) self.assertGreaterEqual(fixation.duration, size * min_time) self.assertLessEqual(fixation.duration, size * max_time) + self.assertLessEqual(fixation.finished, True) # Check first saccade ts, saccade = ts_saccades.pop_first() @@ -207,6 +212,7 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): self.assertEqual(len(saccade.positions.keys()), move) self.assertGreaterEqual(saccade.duration, min_time) self.assertLessEqual(saccade.duration, max_time) + self.assertLessEqual(saccade.finished, True) # Check second fixation ts, fixation = ts_fixations.pop_first() @@ -215,6 +221,7 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): self.assertLessEqual(fixation.deviation_max, deviation_max) self.assertGreaterEqual(fixation.duration, size * min_time) self.assertLessEqual(fixation.duration, size * max_time) + self.assertLessEqual(fixation.finished, True) def test_invalid_gaze_position(self): """Test DispersionThresholdIdentification fixation and saccade identification with invalid gaze position.""" @@ -243,14 +250,16 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): self.assertLessEqual(fixation.deviation_max, deviation_max) self.assertGreaterEqual(fixation.duration, 7 * min_time) self.assertLessEqual(fixation.duration, 7 * max_time) + self.assertLessEqual(fixation.finished, True) - # Check seconde fixation + # Check second fixation ts, fixation = ts_fixations.pop_first() self.assertEqual(len(fixation.positions.keys()), 5) self.assertLessEqual(fixation.deviation_max, deviation_max) self.assertGreaterEqual(fixation.duration, 5 * min_time) self.assertLessEqual(fixation.duration, 5 * max_time) + self.assertLessEqual(fixation.finished, True) def test_fixation_overlapping(self): """Test Fixation overlap function.""" @@ -310,6 +319,7 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): #self.assertGreaterEqual(fixation.deviation_max, deviation_max) self.assertGreaterEqual(fixation.duration, 2 * size * min_time) self.assertLessEqual(fixation.duration, 2 * size * max_time) + self.assertLessEqual(fixation.finished, True) def test_identification_generator(self): """Test DispersionThresholdIdentification identification using generator.""" @@ -338,6 +348,7 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): self.assertLessEqual(gaze_movement.deviation_max, deviation_max) self.assertGreaterEqual(gaze_movement.duration, size * min_time) self.assertLessEqual(gaze_movement.duration, size * max_time) + self.assertLessEqual(gaze_movement.finished, True) fixation_count += 1 @@ -346,6 +357,7 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): self.assertEqual(len(gaze_movement.positions.keys()), 1) self.assertGreaterEqual(gaze_movement.duration, 0.) self.assertLessEqual(gaze_movement.duration, 0.) + self.assertLessEqual(gaze_movement.finished, True) if __name__ == '__main__': diff --git a/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py b/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py index 4cf3a81..71a8daf 100644 --- a/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py +++ b/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py @@ -127,6 +127,7 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase): self.assertEqual(len(fixation.positions.keys()), size-1) self.assertGreaterEqual(fixation.duration, size * min_time) self.assertLessEqual(fixation.duration, size * max_time) + self.assertLessEqual(fixation.finished, True) def test_fixation_and_direct_saccade_identification(self): """Test VelocityThresholdIdentification fixation and saccade identification.""" @@ -158,6 +159,7 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase): self.assertEqual(len(fixation.positions.keys()), size-1) self.assertGreaterEqual(fixation.duration, size * min_time) self.assertLessEqual(fixation.duration, size * max_time) + self.assertLessEqual(fixation.finished, True) # Check first saccade ts, saccade = ts_saccades.pop_first() @@ -165,6 +167,7 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase): self.assertEqual(len(saccade.positions.keys()), 1) self.assertGreaterEqual(saccade.duration, 0.) self.assertLessEqual(saccade.duration, 0.) + self.assertLessEqual(saccade.finished, True) # Check second fixation ts, fixation = ts_fixations.pop_first() @@ -172,6 +175,7 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase): self.assertEqual(len(fixation.positions.keys()), size-1) self.assertGreaterEqual(fixation.duration, size * min_time) self.assertLessEqual(fixation.duration, size * max_time) + self.assertLessEqual(fixation.finished, True) def test_fixation_and_short_saccade_identification(self): """Test VelocityThresholdIdentification fixation and saccade identification.""" @@ -206,6 +210,7 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase): self.assertEqual(len(fixation.positions.keys()), size-1) self.assertGreaterEqual(fixation.duration, size * min_time) self.assertLessEqual(fixation.duration, size * max_time) + self.assertLessEqual(fixation.finished, True) # Check first saccade ts, saccade = ts_saccades.pop_first() @@ -213,6 +218,7 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase): self.assertEqual(len(saccade.positions.keys()), move+1) self.assertGreaterEqual(saccade.duration, min_time) self.assertLessEqual(saccade.duration, max_time) + self.assertLessEqual(saccade.finished, True) # Check second fixation ts, fixation = ts_fixations.pop_first() @@ -220,6 +226,7 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase): self.assertEqual(len(fixation.positions.keys()), size-1) self.assertGreaterEqual(fixation.duration, size * min_time) self.assertLessEqual(fixation.duration, size * max_time) + self.assertLessEqual(fixation.finished, True) def test_invalid_gaze_position(self): """Test VelocityThresholdIdentification fixation and saccade identification with invalid gaze position.""" @@ -248,6 +255,7 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase): self.assertEqual(len(fixation.positions.keys()), 6) self.assertGreaterEqual(fixation.duration, 6 * min_time) self.assertLessEqual(fixation.duration, 6 * max_time) + self.assertLessEqual(fixation.finished, True) # Check second fixation ts, fixation = ts_fixations.pop_first() @@ -255,6 +263,7 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase): self.assertEqual(len(fixation.positions.keys()), 4) self.assertGreaterEqual(fixation.duration, 4 * min_time) self.assertLessEqual(fixation.duration, 4 * max_time) + self.assertLessEqual(fixation.finished, True) if __name__ == '__main__': diff --git a/src/argaze.test/GazeFeatures.py b/src/argaze.test/GazeFeatures.py index dd3f1c0..b8d173c 100644 --- a/src/argaze.test/GazeFeatures.py +++ b/src/argaze.test/GazeFeatures.py @@ -287,6 +287,37 @@ class TestTimeStampedGazePositionsClass(unittest.TestCase): self.assertEqual(ts_gaze_positions_dataframe["value"].dtype, 'object') self.assertEqual(ts_gaze_positions_dataframe["precision"].dtype, 'O') # Python object type +class TestGazeMovementClass(unittest.TestCase): + """Test GazeMovement class.""" + + def test_new(self): + """Test GazeMovement creation.""" + + abstract_gaze_movement = GazeFeatures.GazeMovement(random_gaze_positions(0)) + + # Check abstract GazeMovement + self.assertEqual(len(abstract_gaze_movement.positions), 0) + self.assertEqual(abstract_gaze_movement.duration, -1) + self.assertEqual(abstract_gaze_movement.amplitude, -1) + self.assertEqual(abstract_gaze_movement.valid, False) + self.assertEqual(abstract_gaze_movement.finished, False) + +class TestUnvalidGazeMovementClass(unittest.TestCase): + """Test UnvalidGazeMovement class.""" + + def test_new(self): + """Test UnvalidGazeMovement creation.""" + + unvalid_gaze_movement = GazeFeatures.UnvalidGazeMovement('test') + + # Check UnvalidGazeMovement + self.assertEqual(len(unvalid_gaze_movement.positions), 0) + self.assertEqual(unvalid_gaze_movement.duration, -1) + self.assertEqual(unvalid_gaze_movement.amplitude, -1) + self.assertEqual(unvalid_gaze_movement.valid, False) + self.assertEqual(unvalid_gaze_movement.finished, False) + self.assertEqual(unvalid_gaze_movement.message, 'test') + class TestScanStepClass(unittest.TestCase): """Test ScanStep class.""" diff --git a/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py b/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py index 147046a..3015d51 100644 --- a/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py +++ b/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py @@ -107,7 +107,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): 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.""" + It is also used as maximal duration allowed to wait valid gaze positions.""" def __post_init__(self): @@ -136,6 +136,9 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): # Get last movement last_movement = self.current_saccade if len(self.__fixation_positions) == 0 else self.current_fixation + # Set last movement as finished + last_movement.finish() + # Clear all former gaze positions self.__valid_positions = GazeFeatures.TimeStampedGazePositions() self.__fixation_positions = GazeFeatures.TimeStampedGazePositions() @@ -165,6 +168,9 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): # Store last saccade last_saccade = self.current_saccade + # Set last saccade as finished + last_saccade.finish() + # Clear saccade positions self.__saccade_positions = GazeFeatures.TimeStampedGazePositions() @@ -180,6 +186,9 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): # Store last fixation last_fixation = self.current_fixation + # Set last fixation as finished + last_fixation.finish() + # Start saccade positions with current gaze position self.__saccade_positions[ts] = gaze_position diff --git a/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py b/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py index 7131373..9a645c7 100644 --- a/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py +++ b/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py @@ -153,6 +153,9 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): # Get last movement last_movement = self.current_saccade if len(self.__fixation_positions) == 0 else self.current_fixation + # Set last movement as finished + last_movement.finish() + # Clear all former gaze positions self.__fixation_positions = GazeFeatures.TimeStampedGazePositions() self.__saccade_positions = GazeFeatures.TimeStampedGazePositions() @@ -176,8 +179,12 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): # Does last fixation exist? if len(self.__fixation_positions) > 0: + # Create last fixation last_fixation = Fixation(self.__fixation_positions) + # Set last fixation as finished + last_fixation.finish() + # Clear fixation positions self.__fixation_positions = GazeFeatures.TimeStampedGazePositions() @@ -198,8 +205,12 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): # Does last saccade exist? if len(self.__saccade_positions) > 0: + # Create last saccade last_saccade = Saccade(self.__saccade_positions) + # Set last saccade as finished + last_saccade.finish() + # Clear fixation positions self.__saccade_positions = GazeFeatures.TimeStampedGazePositions() diff --git a/src/argaze/GazeFeatures.py b/src/argaze/GazeFeatures.py index 85b167f..855b90b 100644 --- a/src/argaze/GazeFeatures.py +++ b/src/argaze/GazeFeatures.py @@ -222,6 +222,9 @@ class GazeMovement(): amplitude: float = field(init=False) """Inferred amplitude from first and last positions.""" + finished: bool = field(init=False, default=False) + """Is the movement finished?""" + def __post_init__(self): if self.valid: @@ -267,6 +270,12 @@ class GazeMovement(): return len(self.positions) > 0 + def finish(self): + """Set gaze movement as finished""" + + # Update frozen finished attribute + object.__setattr__(self, 'finished', True) + def draw_positions(self, image: numpy.array, color=(0, 55, 55)): """Draw gaze movement positions""" -- cgit v1.1