From 952c2981598800f399fd09448e478df94aa7703c Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Wed, 24 May 2023 10:21:24 +0200 Subject: Computing transiton matrix directly during AOIScanStep creation. Computing probabilities and density inside TransitionMatrix class. --- src/argaze/GazeAnalysis/TransitionMatrix.py | 14 ++++++++------ src/argaze/GazeFeatures.py | 30 +++++++++++++++++++++++++++-- src/argaze/utils/demo_gaze_features_run.py | 11 +++++++---- 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/argaze/GazeAnalysis/TransitionMatrix.py b/src/argaze/GazeAnalysis/TransitionMatrix.py index 6fd7a7b..dce61de 100644 --- a/src/argaze/GazeAnalysis/TransitionMatrix.py +++ b/src/argaze/GazeAnalysis/TransitionMatrix.py @@ -13,10 +13,11 @@ from dataclasses import dataclass, field from argaze import GazeFeatures import pandas +import numpy @dataclass class AOIScanPathAnalyzer(GazeFeatures.AOIScanPathAnalyzer): - """Implementation of transition matrix probabilities and density algorithm as described in ... + """Implementation of transition matrix probabilities and density algorithm as described in Krejtz et al., 2014 """ def __post_init__(self): @@ -28,13 +29,14 @@ class AOIScanPathAnalyzer(GazeFeatures.AOIScanPathAnalyzer): assert(len(aoi_scan_path) > 1) - sequence = [] + # Sum transitions starting from each aoi + row_sum = aoi_scan_path.transition_matrix.apply(lambda row: row.sum(), axis=1) - for aoi_scan_step in aoi_scan_path: + # Editing transition matrix probabilities + # Note: when no transiton starts from an aoi, destination probabilites is equal to 1/S where S is the number of aois + transition_matrix_probabilities = aoi_scan_path.transition_matrix.apply(lambda row: row.apply(lambda p: p / row_sum[row.name] if row_sum[row.name] > 0 else 1 / row_sum.size), axis=1) - sequence.append(aoi_scan_step.aoi) - - transition_matrix_probabilities = pandas.crosstab(pandas.Series(sequence[1:], name='to'), pandas.Series(sequence[:-1], name='from'), normalize='index') + # Calculate matrix density transition_matrix_density = (transition_matrix_probabilities != 0.).astype(int).sum(axis=1).sum() / transition_matrix_probabilities.size return transition_matrix_probabilities, transition_matrix_density diff --git a/src/argaze/GazeFeatures.py b/src/argaze/GazeFeatures.py index a098dc4..21841ac 100644 --- a/src/argaze/GazeFeatures.py +++ b/src/argaze/GazeFeatures.py @@ -548,18 +548,22 @@ AOIScanPathType = TypeVar('AOIScanPathType', bound="AOIScanPathType") # Type definition for type annotation convenience class AOIScanPath(list): - """List of aoi scan steps over successive AOI.""" + """List of aoi scan steps over successive aoi.""" - def __init__(self): + def __init__(self, expected_aois: list[str] = []): super().__init__() + self.__expected_aois = expected_aois self.__movements = TimeStampedGazeMovements() self.__current_aoi = '' self.__index = ord('A') self.__aoi_letter = {} self.__letter_aoi = {} + size = len(self.__expected_aois) + self.__transition_matrix = pandas.DataFrame(numpy.zeros((size, size)), index=self.__expected_aois, columns=self.__expected_aois) + def __repr__(self): """String representation.""" @@ -593,11 +597,23 @@ class AOIScanPath(list): return sequence @property + def expected_aois(self): + """List of all expected aoi.""" + + return self.__expected_aois + + @property def current_aoi(self): """AOI name of aoi scan step under construction""" return self.__current_aoi + @property + def transition_matrix(self) -> pandas.DataFrame: + """Pandas DataFrame where indexes are transition departures and columns are transition destinations.""" + + return self.__transition_matrix + def append_saccade(self, ts, saccade): """Append new saccade to aoi scan path.""" @@ -612,6 +628,10 @@ class AOIScanPath(list): .. warning:: It could raise AOIScanStepError""" + if looked_aoi not in self.__expected_aois: + + raise AOIScanStepError('AOI not expected', looked_aoi) + # Is it fixation onto a new aoi? if looked_aoi != self.__current_aoi and len(self.__movements) > 0: @@ -626,6 +646,12 @@ class AOIScanPath(list): # Edit new step new_step = AOIScanStep(self.__movements, self.__current_aoi, letter) + # Edit transition matrix + if len(self) > 0: + + # Increment [index: source, columns: destination] value + self.__transition_matrix.loc[self[-1].aoi, self.__current_aoi,] += 1 + # Append new step super().append(new_step) diff --git a/src/argaze/utils/demo_gaze_features_run.py b/src/argaze/utils/demo_gaze_features_run.py index 736ffa9..50cf113 100644 --- a/src/argaze/utils/demo_gaze_features_run.py +++ b/src/argaze/utils/demo_gaze_features_run.py @@ -80,7 +80,7 @@ def main(): identification_mode = 'I-DT' raw_scan_path = GazeFeatures.ScanPath() - aoi_scan_path = GazeFeatures.AOIScanPath() + aoi_scan_path = GazeFeatures.AOIScanPath(aoi_scene_projection.keys()) tm = TransitionMatrix.AOIScanPathAnalyzer() tm_probabilities = pandas.DataFrame() @@ -342,9 +342,11 @@ def main(): cv2.putText(aoi_matrix, f'Transition matrix density: {tm_density:.2f}', (20, window_size[1]-160), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 1, cv2.LINE_AA) - for from_aoi, column in tm_probabilities.items(): + # Iterate over indexes (departures) + for from_aoi, row in tm_probabilities.iterrows(): - for to_aoi, probability in column.items(): + # Iterate over columns (destinations) + for to_aoi, probability in row.items(): if from_aoi != to_aoi and probability > 0.0: @@ -396,7 +398,8 @@ def main(): # Write entropy if enable_entropy_analysis: - cv2.putText(aoi_matrix, f'Entropy: {entropy_analysis}', (20, window_size[1]-240), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 1, cv2.LINE_AA) + cv2.putText(aoi_matrix, f'Stationary entropy: {entropy_analysis[0]:.3f},', (20, window_size[1]-280), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 1, cv2.LINE_AA) + cv2.putText(aoi_matrix, f'Transition entropy: {entropy_analysis[1]:.3f},', (20, window_size[1]-240), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 1, cv2.LINE_AA) # Unlock gaze movement identification gaze_movement_lock.release() -- cgit v1.1