From 49d6cf63d02b80d10c96cfe2edc6fd197558c899 Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Mon, 10 Jul 2023 14:08:30 +0200 Subject: Improving and testing gaze movement identification. Now each movement have to share its last position with the next movement. --- .../DispersionThresholdIdentification.py | 122 +++++++++++++++------ 1 file changed, 88 insertions(+), 34 deletions(-) (limited to 'src/argaze.test') diff --git a/src/argaze.test/GazeAnalysis/DispersionThresholdIdentification.py b/src/argaze.test/GazeAnalysis/DispersionThresholdIdentification.py index f1d02d6..09eb58b 100644 --- a/src/argaze.test/GazeAnalysis/DispersionThresholdIdentification.py +++ b/src/argaze.test/GazeAnalysis/DispersionThresholdIdentification.py @@ -27,6 +27,10 @@ def build_gaze_fixation(size: int, center: tuple, deviation_max: float, min_time for i in range(0, size): + # Sleep a random time + sleep_time = random.random() * (max_time - min_time) + min_time + time.sleep(sleep_time) + # Check position validity valid = True if len(validity) > i: @@ -48,10 +52,6 @@ def build_gaze_fixation(size: int, center: tuple, deviation_max: float, min_time ts = time.time() - start_time + start_ts ts_gaze_positions[ts] = gaze_position - # Sleep a random time - sleep_time = random.random() * (max_time - min_time) + min_time - time.sleep(sleep_time) - return ts_gaze_positions def build_gaze_saccade(size: int, center_A: tuple, center_B: tuple, min_time: float, max_time: float, start_ts: float = 0., validity: list = []): @@ -65,6 +65,10 @@ def build_gaze_saccade(size: int, center_A: tuple, center_B: tuple, min_time: fl for i in range(0, size): + # Sleep a random time + sleep_time = random.random() * (max_time - min_time) + min_time + time.sleep(sleep_time) + # Check position validity valid = True if len(validity) > i: @@ -86,10 +90,6 @@ def build_gaze_saccade(size: int, center_A: tuple, center_B: tuple, min_time: fl ts = time.time() - start_time + start_ts ts_gaze_positions[ts] = gaze_position - # Sleep a random time - sleep_time = random.random() * (max_time - min_time) + min_time - time.sleep(sleep_time) - return ts_gaze_positions class TestDispersionThresholdIdentificationClass(unittest.TestCase): @@ -121,7 +121,7 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): 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.""" @@ -157,20 +157,20 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): # Check first saccade ts, saccade = ts_saccades.pop_first() - self.assertEqual(len(saccade.positions.keys()), 1) - self.assertGreaterEqual(saccade.duration, 0.) - self.assertLessEqual(saccade.duration, 0.) + self.assertEqual(len(saccade.positions.keys()), 2) + self.assertGreaterEqual(saccade.duration, min_time) + self.assertLessEqual(saccade.duration, max_time + min_time) self.assertLessEqual(saccade.finished, True) # Check second fixation ts, fixation = ts_fixations.pop_first() - self.assertEqual(len(fixation.positions.keys()), size-1) + self.assertEqual(len(fixation.positions.keys()), size) 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.""" @@ -209,9 +209,9 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): # Check first saccade ts, saccade = ts_saccades.pop_first() - self.assertEqual(len(saccade.positions.keys()), move) + self.assertEqual(len(saccade.positions.keys()), move + 2) self.assertGreaterEqual(saccade.duration, min_time) - self.assertLessEqual(saccade.duration, max_time) + self.assertLessEqual(saccade.duration, max_time + 2 * min_time) self.assertLessEqual(saccade.finished, True) # Check second fixation @@ -222,7 +222,7 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): 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.""" @@ -260,7 +260,7 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): 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.""" @@ -321,6 +321,64 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): self.assertLessEqual(fixation.duration, 2 * size * max_time) self.assertLessEqual(fixation.finished, True) + def test_identification_browsing(self): + """Test DispersionThresholdIdentification identification browsing.""" + + size = 10 + center_A = (0, 0) + center_B = (50, 50) + deviation_max = 10 + min_time = 0.01 + max_time = 0.1 + + ts_gaze_positions_A = build_gaze_fixation(size, center_A, deviation_max, min_time, max_time) + ts_gaze_positions_B = build_gaze_fixation(size, center_B, deviation_max, min_time, max_time, start_ts=ts_gaze_positions_A.last[0]) + + ts_gaze_positions = ts_gaze_positions_A.append(ts_gaze_positions_B) + + gaze_movement_identifier = DispersionThresholdIdentification.GazeMovementIdentifier(deviation_max_threshold=deviation_max, duration_min_threshold=max_time*2) + + # Get last ts to terminate identification on last gaze position + last_ts, _ = ts_gaze_positions.last + + # Iterate on gaze positions + for ts, gaze_position in ts_gaze_positions.items(): + + finished_gaze_movement = gaze_movement_identifier.identify(ts, gaze_position, terminate=(ts == last_ts)) + + if GazeFeatures.is_fixation(finished_gaze_movement): + + self.assertEqual(len(finished_gaze_movement.positions.keys()), size) + self.assertLessEqual(finished_gaze_movement.deviation_max, deviation_max) + self.assertGreaterEqual(finished_gaze_movement.duration, size * min_time) + self.assertLessEqual(finished_gaze_movement.duration, size * max_time) + self.assertLessEqual(finished_gaze_movement.finished, True) + + elif GazeFeatures.is_saccade(finished_gaze_movement): + + self.assertEqual(len(finished_gaze_movement.positions.keys()), 2) + self.assertGreaterEqual(finished_gaze_movement.duration, min_time) + self.assertLessEqual(finished_gaze_movement.duration, max_time + min_time) + self.assertLessEqual(finished_gaze_movement.finished, True) + + # Check that last gaze position date is not equal to given gaze position date + if finished_gaze_movement.valid: + + last_ts, _ = finished_gaze_movement.positions.last + + self.assertNotEqual(last_ts, ts) + + # Check that last gaze position date of current fixation is equal to given gaze position date + # NOTE: This is not true for saccade as there is a minimal time window while the gaze movement is unknown + current_gaze_movement = gaze_movement_identifier.current_gaze_movement + if current_gaze_movement.valid: + + if GazeFeatures.is_fixation(current_gaze_movement): + + last_ts, _ = current_gaze_movement.positions.last + + self.assertEqual(last_ts, ts) + def test_identification_generator(self): """Test DispersionThresholdIdentification identification using generator.""" @@ -337,27 +395,23 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): ts_gaze_positions = ts_gaze_positions_A.append(ts_gaze_positions_B) gaze_movement_identifier = DispersionThresholdIdentification.GazeMovementIdentifier(deviation_max_threshold=deviation_max, duration_min_threshold=max_time*2) - - fixation_count = 0 - - for ts, gaze_movement in gaze_movement_identifier(ts_gaze_positions): - if GazeFeatures.is_fixation(gaze_movement): + for ts, finished_gaze_movement in gaze_movement_identifier(ts_gaze_positions): - self.assertEqual(len(gaze_movement.positions.keys()), size-fixation_count) - 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) + if GazeFeatures.is_fixation(finished_gaze_movement): - fixation_count += 1 + self.assertEqual(len(finished_gaze_movement.positions.keys()), size) + self.assertLessEqual(finished_gaze_movement.deviation_max, deviation_max) + self.assertGreaterEqual(finished_gaze_movement.duration, size * min_time) + self.assertLessEqual(finished_gaze_movement.duration, size * max_time) + self.assertLessEqual(finished_gaze_movement.finished, True) - elif GazeFeatures.is_saccade(gaze_movement): + elif GazeFeatures.is_saccade(finished_gaze_movement): - 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) + self.assertEqual(len(finished_gaze_movement.positions.keys()), 2) + self.assertGreaterEqual(finished_gaze_movement.duration, min_time) + self.assertLessEqual(finished_gaze_movement.duration, max_time + min_time) + self.assertLessEqual(finished_gaze_movement.finished, True) if __name__ == '__main__': -- cgit v1.1