From f861c25ed56bf3204188ecc9170feb21e613794e Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Wed, 31 May 2023 16:02:47 +0200 Subject: Don't ignore last movement on invalid gaze position. --- .../DispersionThresholdIdentification.py | 79 +++++++++++----------- .../DispersionThresholdIdentification.py | 13 +++- 2 files changed, 52 insertions(+), 40 deletions(-) (limited to 'src') diff --git a/src/argaze.test/GazeAnalysis/DispersionThresholdIdentification.py b/src/argaze.test/GazeAnalysis/DispersionThresholdIdentification.py index dc4284c..712308a 100644 --- a/src/argaze.test/GazeAnalysis/DispersionThresholdIdentification.py +++ b/src/argaze.test/GazeAnalysis/DispersionThresholdIdentification.py @@ -16,13 +16,15 @@ from argaze.GazeAnalysis import DispersionThresholdIdentification import numpy -def build_gaze_fixation(size: int, center: tuple, deviation_max: float, start_time: float, min_time: float, max_time: float, validity: list = []): +def build_gaze_fixation(size: int, center: tuple, deviation_max: float, min_time: float, max_time: float, start_ts: float = 0., validity: list = []): """ Generate N TimeStampedGazePsoitions dispersed around a center point for testing purpose. Timestamps are current time after random sleep (second). GazePositions are random values. """ ts_gaze_positions = GazeFeatures.TimeStampedGazePositions() + start_time = time.time() + for i in range(0, size): # Check position validity @@ -43,7 +45,7 @@ def build_gaze_fixation(size: int, center: tuple, deviation_max: float, start_ti gaze_position = GazeFeatures.UnvalidGazePosition() # Store gaze position - ts = time.time() - start_time + ts = time.time() - start_time + start_ts ts_gaze_positions[ts] = gaze_position # Sleep a random time @@ -52,13 +54,15 @@ def build_gaze_fixation(size: int, center: tuple, deviation_max: float, start_ti return ts_gaze_positions -def build_gaze_saccade(size: int, center_A: tuple, center_B: tuple, start_time: float, min_time: float, max_time: float, validity: list = []): +def build_gaze_saccade(size: int, center_A: tuple, center_B: tuple, min_time: float, max_time: float, start_ts: float = 0., validity: list = []): """ Generate N TimeStampedGazePsoitions between 2 center points for testing purpose. Timestamps are current time after random sleep (second). GazePositions are random values. """ ts_gaze_positions = GazeFeatures.TimeStampedGazePositions() + start_time = time.time() + for i in range(0, size): # Check position validity @@ -79,7 +83,7 @@ def build_gaze_saccade(size: int, center_A: tuple, center_B: tuple, start_time: gaze_position = GazeFeatures.UnvalidGazePosition() # Store gaze position - ts = time.time() - start_time + ts = time.time() - start_time + start_ts ts_gaze_positions[ts] = gaze_position # Sleep a random time @@ -97,11 +101,10 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): size = 10 center = (0, 0) deviation_max = 10 - start_time = time.time() min_time = 0.01 max_time = 0.1 - ts_gaze_positions = build_gaze_fixation(size, center, deviation_max, start_time, min_time, max_time) + ts_gaze_positions = build_gaze_fixation(size, center, deviation_max, min_time, max_time) gaze_movement_identifier = DispersionThresholdIdentification.GazeMovementIdentifier(deviation_max_threshold=deviation_max, duration_min_threshold=max_time*2) ts_fixations, ts_saccades, ts_status = gaze_movement_identifier.browse(ts_gaze_positions) @@ -125,12 +128,11 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): center_A = (0, 0) center_B = (50, 50) deviation_max = 10 - start_time = time.time() min_time = 0.01 max_time = 0.1 - ts_gaze_positions_A = build_gaze_fixation(size, center_A, deviation_max, start_time, min_time, max_time) - ts_gaze_positions_B = build_gaze_fixation(size, center_B, deviation_max, start_time, min_time, max_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]) ts_gaze_positions = ts_gaze_positions_A.append(ts_gaze_positions_B) @@ -164,7 +166,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) - ''' + def test_fixation_and_short_saccade_identification(self): """Test DispersionThresholdIdentification fixation and saccade identification.""" @@ -174,15 +176,12 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): out_A = (20, 20) center_B = (50, 50) deviation_max = 10 - start_time = time.time() min_time = 0.01 max_time = 0.1 - ts_gaze_positions_A = build_gaze_fixation(size, center_A, deviation_max, start_time, min_time, max_time) - ts_move_positions = build_gaze_saccade(move, out_A, center_B, start_time, min_time, min_time) - ts_gaze_positions_B = build_gaze_fixation(size, center_B, deviation_max, start_time, min_time, max_time) - - print(ts_move_positions) + 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]) + ts_gaze_positions_B = build_gaze_fixation(size, center_B, deviation_max, min_time, max_time, start_ts=ts_move_positions.last[0]) ts_gaze_positions = ts_gaze_positions_A.append(ts_move_positions).append(ts_gaze_positions_B) @@ -197,7 +196,7 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): # Check first 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) @@ -205,48 +204,54 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): # Check first saccade ts, saccade = ts_saccades.pop_first() - self.assertEqual(len(saccade.positions.keys()), move + 2) + self.assertEqual(len(saccade.positions.keys()), move) self.assertGreaterEqual(saccade.duration, min_time) self.assertLessEqual(saccade.duration, max_time) # 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) - ''' + def test_invalid_gaze_position(self): """Test DispersionThresholdIdentification fixation and saccade identification with invalid gaze position.""" size = 15 center = (0, 0) deviation_max = 10 - start_time = time.time() - min_time = 0.01 + min_time = 0.05 max_time = 0.1 - validity = [True, True, True, True, False, True, True, True, False, False, True, True, True, True, True] + validity = [True, True, True, True, True, True, True, False, False, False, True, True, True, True, True] - ts_gaze_positions = build_gaze_fixation(size, center, deviation_max, start_time, min_time, max_time, validity) + ts_gaze_positions = build_gaze_fixation(size, center, deviation_max, min_time, max_time, validity=validity) gaze_movement_identifier = DispersionThresholdIdentification.GazeMovementIdentifier(deviation_max_threshold=deviation_max, duration_min_threshold=max_time*2) ts_fixations, ts_saccades, ts_status = gaze_movement_identifier.browse(ts_gaze_positions) # Check result size - self.assertEqual(len(ts_fixations), 1) + self.assertEqual(len(ts_fixations), 2) self.assertEqual(len(ts_saccades), 0) self.assertEqual(len(ts_status), size-3) - # Check fixation + # Check first fixation ts, fixation = ts_fixations.pop_first() - self.assertEqual(len(fixation.positions.keys()), size-3) + self.assertEqual(len(fixation.positions.keys()), 7) self.assertLessEqual(fixation.deviation_max, deviation_max) - self.assertGreaterEqual(fixation.duration, size * min_time) - self.assertLessEqual(fixation.duration, size * max_time) + self.assertGreaterEqual(fixation.duration, 7 * min_time) + self.assertLessEqual(fixation.duration, 7 * max_time) + + # Check seconde 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) - @unittest.skip("Fixation overlapping is broken.") def test_fixation_overlapping(self): """Test Fixation overlap function.""" @@ -255,13 +260,12 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): center_B = (50, 50) center_C = (55, 55) deviation_max = 10 - start_time = time.time() min_time = 0.01 max_time = 0.1 - ts_gaze_positions_A = build_gaze_fixation(size, center_A, deviation_max, start_time, min_time, max_time) - ts_gaze_positions_B = build_gaze_fixation(size, center_B, deviation_max, start_time, min_time, max_time) - ts_gaze_positions_C = build_gaze_fixation(size, center_C, deviation_max, start_time, min_time, max_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]) + ts_gaze_positions_C = build_gaze_fixation(size, center_C, deviation_max, min_time, max_time, start_ts=ts_gaze_positions_B.last[0]) fixation_A = DispersionThresholdIdentification.Fixation(ts_gaze_positions_A) fixation_B = DispersionThresholdIdentification.Fixation(ts_gaze_positions_B) @@ -275,7 +279,7 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): self.assertTrue(fixation_B.overlap(fixation_C)) self.assertTrue(fixation_C.overlap(fixation_B)) - @unittest.skip("Fixation overlapping identification is broken.") + @unittest.skip("Fixation overlapping is not supported anymore.") def test_fixation_overlapping_identification(self): """Test DispersionThresholdIdentification identification when fixations overlap.""" @@ -283,12 +287,11 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): center_A = (-5, 0) center_B = (5, 0) deviation_max = 15 - start_time = time.time() min_time = 0.01 max_time = 0.1 - ts_gaze_positions_A = build_gaze_fixation(size, center_A, deviation_max, start_time, min_time, max_time) - ts_gaze_positions_B = build_gaze_fixation(size, center_B, deviation_max, start_time, min_time, max_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]) ts_gaze_positions = ts_gaze_positions_A.append(ts_gaze_positions_B) diff --git a/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py b/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py index c6353d4..9d5a8d7 100644 --- a/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py +++ b/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py @@ -57,7 +57,7 @@ class Fixation(GazeFeatures.Fixation): def point_deviation(self, gaze_position) -> float: """Get distance of a point from the fixation's centroïd.""" - return numpy.sqrt((self.centroid[0] - gaze_position.value[0])**2 + (self.centroid[1] - gaze_position.value[1])**2) + return numpy.sqrt((self.focus[0] - gaze_position.value[0])**2 + (self.focus[1] - gaze_position.value[1])**2) def overlap(self, fixation) -> bool: """Does a gaze position from another fixation having a deviation to this fixation centroïd smaller than maximal deviation?""" @@ -65,7 +65,7 @@ class Fixation(GazeFeatures.Fixation): points = fixation.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([self.centroid[0], self.centroid[1]]) + centroid_array = numpy.array([self.focus[0], self.focus[1]]) deviations_array = numpy.sqrt(numpy.sum((points_array - centroid_array)**2, axis=1)) return min(deviations_array) <= self.deviation_max @@ -133,11 +133,20 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): if (ts - ts_last) > self.duration_min_threshold: + # Get last movement + last_movement = self.current_saccade if len(self.__fixation_positions) == 0 else self.current_fixation + # Clear all former gaze positions self.__valid_positions = GazeFeatures.TimeStampedGazePositions() self.__fixation_positions = GazeFeatures.TimeStampedGazePositions() self.__saccade_positions = GazeFeatures.TimeStampedGazePositions() + # Store valid gaze position + self.__valid_positions[ts] = gaze_position + + # Return last valid movement if exist + return last_movement + # Store gaze positions until a minimal duration self.__valid_positions[ts] = gaze_position -- cgit v1.1