diff options
-rw-r--r-- | src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py | 112 | ||||
-rw-r--r-- | src/argaze/GazeAnalysis/VelocityThresholdIdentification.py | 47 |
2 files changed, 94 insertions, 65 deletions
diff --git a/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py b/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py index 71a8daf..239bf45 100644 --- a/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py +++ b/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py @@ -30,6 +30,10 @@ def build_gaze_fixation(size: int, start_position: tuple, deviation_max: float, 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: @@ -39,8 +43,8 @@ def build_gaze_fixation(size: int, start_position: tuple, deviation_max: float, if valid: # Edit gaze position - random_x = last_valid_position[0] + deviation_max * (random.random() - 0.5) - random_y = last_valid_position[1] + deviation_max * (random.random() - 0.5) + random_x = last_valid_position[0] + deviation_max * (random.random() - 0.5) / math.sqrt(2) + random_y = last_valid_position[1] + deviation_max * (random.random() - 0.5) / math.sqrt(2) gaze_position = GazeFeatures.GazePosition((random_x, random_y)) @@ -55,10 +59,6 @@ def build_gaze_fixation(size: int, start_position: tuple, deviation_max: float, 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 = []): @@ -72,6 +72,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: @@ -93,10 +97,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 TestVelocityThresholdIdentificationClass(unittest.TestCase): @@ -119,16 +119,16 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase): # Check result size self.assertEqual(len(ts_fixations), 1) self.assertEqual(len(ts_saccades), 0) - self.assertEqual(len(ts_status), size-1) + self.assertEqual(len(ts_status), size - 1) # Check fixation ts, fixation = ts_fixations.pop_first() - self.assertEqual(len(fixation.positions.keys()), size-1) - self.assertGreaterEqual(fixation.duration, size * min_time) - self.assertLessEqual(fixation.duration, size * max_time) + self.assertEqual(len(fixation.positions.keys()), size - 1) + self.assertGreaterEqual(fixation.duration, (size - 2) * min_time) + self.assertLessEqual(fixation.duration, (size - 2) * max_time) self.assertLessEqual(fixation.finished, True) - + def test_fixation_and_direct_saccade_identification(self): """Test VelocityThresholdIdentification fixation and saccade identification.""" @@ -138,7 +138,7 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase): deviation_max = 10 min_time = 0.05 max_time = 0.1 - velocity_max = math.sqrt(2) * deviation_max / min_time + velocity_max = deviation_max / min_time 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]) @@ -151,32 +151,46 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase): # Check result size self.assertEqual(len(ts_fixations), 2) self.assertEqual(len(ts_saccades), 1) - self.assertEqual(len(ts_status), size*2 - 1) + self.assertEqual(len(ts_status), size * 2 - 1) # Check first fixation ts, fixation = ts_fixations.pop_first() - self.assertEqual(len(fixation.positions.keys()), size-1) - self.assertGreaterEqual(fixation.duration, size * min_time) - self.assertLessEqual(fixation.duration, size * max_time) + self.assertEqual(len(fixation.positions.keys()), size - 1) + self.assertGreaterEqual(fixation.duration, (size - 2) * min_time) + self.assertLessEqual(fixation.duration, (size - 2) * max_time) self.assertLessEqual(fixation.finished, True) # 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) self.assertLessEqual(saccade.finished, True) + # Check that last position of a movement is equal to first position of next movement + last_ts, last_position = fixation.positions.last + first_ts, first_position = saccade.positions.first + + self.assertEqual(last_ts, first_ts) + self.assertEqual(last_position.value, first_position.value) + # Check second fixation ts, fixation = ts_fixations.pop_first() - self.assertEqual(len(fixation.positions.keys()), size-1) - self.assertGreaterEqual(fixation.duration, size * min_time) - self.assertLessEqual(fixation.duration, size * max_time) + self.assertEqual(len(fixation.positions.keys()), size) + self.assertGreaterEqual(fixation.duration, (size - 1) * min_time) + self.assertLessEqual(fixation.duration, (size - 1) * max_time) self.assertLessEqual(fixation.finished, True) - + + # Check that last position of a movement is equal to first position of next movement + last_ts, last_position = saccade.positions.last + first_ts, first_position = fixation.positions.first + + self.assertEqual(last_ts, first_ts) + self.assertEqual(last_position.value, first_position.value) + def test_fixation_and_short_saccade_identification(self): """Test VelocityThresholdIdentification fixation and saccade identification.""" @@ -188,7 +202,7 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase): deviation_max = 10 min_time = 0.05 max_time = 0.1 - velocity_max = math.sqrt(2) * deviation_max / min_time + velocity_max = deviation_max / min_time ts_gaze_positions_A = build_gaze_fixation(size, center_A, deviation_max, min_time, max_time) ts_move_positions = build_gaze_saccade(move, out_A, center_B, min_time, min_time, start_ts=ts_gaze_positions_A.last[0]) @@ -202,32 +216,46 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase): # Check result size self.assertEqual(len(ts_fixations), 2) self.assertEqual(len(ts_saccades), 1) - self.assertEqual(len(ts_status), 2 * size - 1 + move) + self.assertEqual(len(ts_status), 2 * size + move - 1) # Check first fixation ts, fixation = ts_fixations.pop_first() - self.assertEqual(len(fixation.positions.keys()), size-1) - self.assertGreaterEqual(fixation.duration, size * min_time) - self.assertLessEqual(fixation.duration, size * max_time) + self.assertEqual(len(fixation.positions.keys()), size - 1) + self.assertGreaterEqual(fixation.duration, (size - 2) * min_time) + self.assertLessEqual(fixation.duration, (size - 2) * max_time) self.assertLessEqual(fixation.finished, True) # Check first saccade ts, saccade = ts_saccades.pop_first() - self.assertEqual(len(saccade.positions.keys()), move+1) - self.assertGreaterEqual(saccade.duration, min_time) - self.assertLessEqual(saccade.duration, max_time) + self.assertEqual(len(saccade.positions.keys()), move + 2) + self.assertGreaterEqual(saccade.duration, (move + 1) * min_time) + self.assertLessEqual(saccade.duration, (move + 1) * max_time) self.assertLessEqual(saccade.finished, True) + # Check that last position of a movement is equal to first position of next movement + last_ts, last_position = fixation.positions.last + first_ts, first_position = saccade.positions.first + + self.assertEqual(last_ts, first_ts) + self.assertEqual(last_position.value, first_position.value) + # Check second fixation ts, fixation = ts_fixations.pop_first() - self.assertEqual(len(fixation.positions.keys()), size-1) - self.assertGreaterEqual(fixation.duration, size * min_time) - self.assertLessEqual(fixation.duration, size * max_time) + self.assertEqual(len(fixation.positions.keys()), size) + self.assertGreaterEqual(fixation.duration, (size - 1) * min_time) + self.assertLessEqual(fixation.duration, (size - 1) * max_time) self.assertLessEqual(fixation.finished, True) + # Check that last position of a movement is equal to first position of next movement + last_ts, last_position = saccade.positions.last + first_ts, first_position = fixation.positions.first + + self.assertEqual(last_ts, first_ts) + self.assertEqual(last_position.value, first_position.value) + def test_invalid_gaze_position(self): """Test VelocityThresholdIdentification fixation and saccade identification with invalid gaze position.""" @@ -253,16 +281,16 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase): ts, fixation = ts_fixations.pop_first() self.assertEqual(len(fixation.positions.keys()), 6) - self.assertGreaterEqual(fixation.duration, 6 * min_time) - self.assertLessEqual(fixation.duration, 6 * max_time) + self.assertGreaterEqual(fixation.duration, 5 * min_time) + self.assertLessEqual(fixation.duration, 5 * max_time) self.assertLessEqual(fixation.finished, True) # Check second fixation ts, fixation = ts_fixations.pop_first() self.assertEqual(len(fixation.positions.keys()), 4) - self.assertGreaterEqual(fixation.duration, 4 * min_time) - self.assertLessEqual(fixation.duration, 4 * max_time) + self.assertGreaterEqual(fixation.duration, 3 * min_time) + self.assertLessEqual(fixation.duration, 3 * max_time) self.assertLessEqual(fixation.finished, True) if __name__ == '__main__': diff --git a/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py b/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py index 2b29492..4f79b7e 100644 --- a/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py +++ b/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py @@ -113,8 +113,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): """Maximal velocity 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.""" + """Minimal duration allowed to wait valid gaze positions.""" def __post_init__(self): @@ -141,7 +140,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): self.__last_ts = ts self.__last_position = gaze_position - return + return GazeFeatures.UnvalidGazeMovement() # Check if too much time elapsed since last gaze position if (ts - self.__last_ts) > self.duration_min_threshold: @@ -151,7 +150,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): self.__last_position = gaze_position # Get last movement - last_movement = self.current_saccade.finish() if len(self.__fixation_positions) == 0 else self.current_fixation.finish() + last_movement = self.current_gaze_movement.finish() # Clear all former gaze positions self.__fixation_positions = GazeFeatures.TimeStampedGazePositions() @@ -170,49 +169,51 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): # Velocity is greater than threshold if velocity > self.velocity_max_threshold: - # Append to saccade positions - self.__saccade_positions[ts] = gaze_position + last_fixation = GazeFeatures.UnvalidGazeMovement() # Does last fixation exist? if len(self.__fixation_positions) > 0: + # Copy most recent fixation position into saccade positions + last_ts, last_position = self.__fixation_positions.last + self.__saccade_positions[last_ts] = last_position + # Create last fixation - last_fixation = Fixation(self.__fixation_positions).finish() + last_fixation = self.current_fixation.finish() # Clear fixation positions self.__fixation_positions = GazeFeatures.TimeStampedGazePositions() - # Output last fixation - return last_fixation - - # Identification must stop: ends with current saccade - if terminate: + # Append to saccade positions + self.__saccade_positions[ts] = gaze_position - return self.current_saccade.finish() + # Output last fixation + return last_fixation if not terminate else self.current_saccade.finish() # Velocity is less or equals to threshold else: - # Append to fixation positions - self.__fixation_positions[ts] = gaze_position + last_saccade = GazeFeatures.UnvalidGazeMovement() # Does last saccade exist? if len(self.__saccade_positions) > 0: + # Copy most recent saccade position into fixation positions + last_ts, last_position = self.__saccade_positions.last + self.__fixation_positions[last_ts] = last_position + # Create last saccade - last_saccade = Saccade(self.__saccade_positions).finish() + last_saccade = self.current_saccade.finish() # Clear fixation positions self.__saccade_positions = GazeFeatures.TimeStampedGazePositions() - # Output last saccade - return last_saccade - - # Identification must stop: ends with current fixation - if terminate: - - return self.current_fixation.finish() + # Append to fixation positions + self.__fixation_positions[ts] = gaze_position + # Output last saccade + return last_saccade if not terminate else self.current_fixation.finish() + # Always return unvalid gaze movement at least return GazeFeatures.UnvalidGazeMovement() |