From acf3fabf7c60aa486c0b8c264283ee449a221326 Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Wed, 12 Jul 2023 12:32:04 +0200 Subject: Adding and testing aoi/scan path and scan step durations. --- src/argaze.test/GazeFeatures.py | 53 ++++++++++++++++++---------- src/argaze/GazeFeatures.py | 78 ++++++++++++++++++++++++++++++++++------- 2 files changed, 101 insertions(+), 30 deletions(-) (limited to 'src') diff --git a/src/argaze.test/GazeFeatures.py b/src/argaze.test/GazeFeatures.py index 85ba362..a6709cb 100644 --- a/src/argaze.test/GazeFeatures.py +++ b/src/argaze.test/GazeFeatures.py @@ -379,6 +379,7 @@ class TestScanPathClass(unittest.TestCase): scan_path = GazeFeatures.ScanPath() self.assertEqual(len(scan_path), 0) + self.assertEqual(scan_path.duration, 0) def test_append(self): """Test ScanPath append methods.""" @@ -393,6 +394,7 @@ class TestScanPathClass(unittest.TestCase): # Check that no scan step have been created yet self.assertEqual(len(scan_path), 0) + self.assertEqual(scan_path.duration, 0) self.assertEqual(new_step, None) # Append first fixation @@ -403,18 +405,21 @@ class TestScanPathClass(unittest.TestCase): # Check that no scan step have been created yet self.assertEqual(len(scan_path), 0) + self.assertEqual(scan_path.duration, 0) self.assertEqual(new_step, None) # Append consecutive saccade saccade_A = TestSaccade(random_gaze_positions(2)) ts, _ = saccade_A.positions.first - new_step = scan_path.append_saccade(ts, saccade_A) + new_step_A = scan_path.append_saccade(ts, saccade_A) # Check that new scan step have been created self.assertEqual(len(scan_path), 1) - self.assertEqual(new_step.first_fixation, fixation_A) - self.assertEqual(new_step.last_saccade, saccade_A) + self.assertEqual(scan_path.duration, new_step_A.duration) + self.assertEqual(new_step_A.first_fixation, fixation_A) + self.assertEqual(new_step_A.last_saccade, saccade_A) + self.assertEqual(new_step_A.duration, fixation_A.duration + saccade_A.duration) # Append 2 consecutive fixations then a saccade fixation_B1 = TestFixation(random_gaze_positions(10)) @@ -424,6 +429,7 @@ class TestScanPathClass(unittest.TestCase): # Check that no scan step have been created yet self.assertEqual(len(scan_path), 1) + self.assertEqual(scan_path.duration, new_step_A.duration) self.assertEqual(new_step, None) fixation_B2 = TestFixation(random_gaze_positions(10)) @@ -433,17 +439,20 @@ class TestScanPathClass(unittest.TestCase): # Check that no scan step have been created yet self.assertEqual(len(scan_path), 1) + self.assertEqual(scan_path.duration, new_step_A.duration) self.assertEqual(new_step, None) saccade_B = TestSaccade(random_gaze_positions(2)) ts, _ = saccade_B.positions.first - new_step = scan_path.append_saccade(ts, saccade_B) + new_step_B = scan_path.append_saccade(ts, saccade_B) # Check that new scan step have been created self.assertEqual(len(scan_path), 2) - self.assertEqual(new_step.first_fixation, fixation_B2) - self.assertEqual(new_step.last_saccade, saccade_B) + self.assertEqual(scan_path.duration, new_step_A.duration + new_step_B.duration) + self.assertEqual(new_step_B.first_fixation, fixation_B2) + self.assertEqual(new_step_B.last_saccade, saccade_B) + self.assertEqual(new_step_B.duration, fixation_B2.duration + saccade_B.duration) class TestAOIScanStepClass(unittest.TestCase): """Test AOIScanStep class.""" @@ -525,36 +534,44 @@ class TestAOIScanPathClass(unittest.TestCase): aoi_scan_path = GazeFeatures.AOIScanPath(['Foo', 'Bar']) # Append fixation on A aoi - fixation = TestFixation(random_gaze_positions(10)) - ts, _ = fixation.positions.first + fixation_A = TestFixation(random_gaze_positions(10)) + ts, _ = fixation_A.positions.first - new_step = aoi_scan_path.append_fixation(ts, fixation, 'Foo') + new_step = aoi_scan_path.append_fixation(ts, fixation_A, 'Foo') # Check that no aoi scan step have been created yet self.assertEqual(len(aoi_scan_path), 0) + self.assertEqual(aoi_scan_path.duration, 0) self.assertEqual(new_step, None) # Append saccade - saccade = TestSaccade(random_gaze_positions(2)) - ts, _ = saccade.positions.first + saccade_A = TestSaccade(random_gaze_positions(2)) + ts, _ = saccade_A.positions.first - new_step = aoi_scan_path.append_saccade(ts, saccade) + new_step = aoi_scan_path.append_saccade(ts, saccade_A) # Check that no aoi scan step have been created yet self.assertEqual(len(aoi_scan_path), 0) + self.assertEqual(aoi_scan_path.duration, 0) self.assertEqual(new_step, None) # Append fixation on B aoi - fixation = TestFixation(random_gaze_positions(10)) - ts, _ = fixation.positions.first + fixation_B = TestFixation(random_gaze_positions(10)) + ts, _ = fixation_B.positions.first - new_step = aoi_scan_path.append_fixation(ts, fixation, 'Bar') + new_step_A = aoi_scan_path.append_fixation(ts, fixation_B, 'Bar') # Check a first aoi scan step have been created once a new fixation is appened self.assertEqual(len(aoi_scan_path), 1) - self.assertEqual(len(new_step.movements), 2) - self.assertEqual(new_step.aoi, 'Foo') - self.assertEqual(new_step.letter, 'A') + self.assertEqual(aoi_scan_path.duration, new_step_A.duration) + self.assertEqual(len(new_step_A.movements), 2) + self.assertEqual(new_step_A.aoi, 'Foo') + self.assertEqual(new_step_A.letter, 'A') + + first_ts, _ = fixation_A.positions.first + last_ts, _ = saccade_A.positions.last + + self.assertEqual(new_step_A.duration, last_ts - first_ts) # Check letter affectation self.assertEqual(aoi_scan_path.get_letter_aoi('A'), 'Foo') diff --git a/src/argaze/GazeFeatures.py b/src/argaze/GazeFeatures.py index 315aaf8..8afe2eb 100644 --- a/src/argaze/GazeFeatures.py +++ b/src/argaze/GazeFeatures.py @@ -539,8 +539,10 @@ class ScanStepError(Exception): @dataclass(frozen=True) class ScanStep(): """Define a scan step as a fixation and a consecutive saccade. - .. warning:: - Scan step have to start by a fixation and then end by a saccade.""" + + !!! warning + Scan step have to start by a fixation and then end by a saccade. + """ first_fixation: Fixation """A fixation that comes before the next saccade.""" @@ -561,16 +563,24 @@ class ScanStep(): raise ScanStepError('Last step movement is not a saccade') @property - def duration(self): - """Time spent on AOI.""" + def fixation_duration(self) -> int|float: + """Time spent on AOI - # Timestamp of first position of first fixation - first_ts, _ = self.first_fixation.positions.first + Returns: + fixation duration + """ - # Timestamp of first position of last saccade - last_ts, _ = self.last_saccade.positions.first + return self.first_fixation.duration - return last_ts - first_ts + @property + def duration(self) -> int|float: + """Time spent on AOI and time spent to go to next AOI + + Returns: + duration + """ + + return self.first_fixation.duration + self.last_saccade.duration ScanPathType = TypeVar('ScanPathType', bound="ScanPathType") # Type definition for type annotation convenience @@ -583,6 +593,17 @@ class ScanPath(list): super().__init__() self.__last_fixation = None + self.__duration = 0 + + @property + def duration(self) -> int|float: + """Sum of all scan steps duration + + Returns: + duration + """ + + return self.__duration def append_saccade(self, ts, saccade) -> ScanStepType: """Append new saccade to scan path and return last new scan step if one have been created.""" @@ -598,6 +619,9 @@ class ScanPath(list): # Append new step super().append(new_step) + # Update duration + self.__duration += new_step.duration + # Return new step return new_step @@ -609,7 +633,7 @@ class ScanPath(list): def append_fixation(self, ts, fixation): """Append new fixation to scan path. !!! warning - Consecutives fixations are ignored keeping the last fixation""" + Consecutives fixations are ignored keeping the last fixation""" self.__last_fixation = fixation @@ -706,8 +730,12 @@ class AOIScanStep(): return last_movement @property - def duration(self): - """Time spent on AOI.""" + def fixation_duration(self) -> int|float: + """Time spent on AOI + + Returns: + fixation duration + """ # Timestamp of first position of first fixation first_ts, _ = self.first_fixation.positions.first @@ -717,6 +745,22 @@ class AOIScanStep(): return last_ts - first_ts + @property + def duration(self) -> int|float: + """Time spent on AOI and time spent to go to next AOI + + Returns: + duration + """ + + # Timestamp of first position of first fixation + first_ts, _ = self.first_fixation.positions.first + + # Timestamp of last position of last saccade + last_ts, _ = self.last_saccade.positions.last + + return last_ts - first_ts + AOIScanPathType = TypeVar('AOIScanPathType', bound="AOIScanPathType") # Type definition for type annotation convenience @@ -728,12 +772,19 @@ class AOIScanPath(list): super().__init__() self.expected_aois = expected_aois + self.__duration = 0 def __repr__(self): """String representation.""" return str(super()) + @property + def duration(self) -> float: + """Sum of all scan steps duration""" + + return self.__duration + def __get_aoi_letter(self, aoi): try : @@ -839,6 +890,9 @@ class AOIScanPath(list): # Append new step super().append(new_step) + # Update duration + self.__duration += new_step.duration + # Return new step return new_step -- cgit v1.1