aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThéo de la Hogue2022-12-10 18:30:30 +0100
committerThéo de la Hogue2022-12-10 18:30:30 +0100
commit1b9957ab30697c57370469b5334e48ce4fe9202e (patch)
treee38ccd1d59ef4271577d0594905548d6e6ddf6aa
parentd74c6c9ffc05a13ed7485ddcbdfa18ad5b6bf59b (diff)
downloadargaze-1b9957ab30697c57370469b5334e48ce4fe9202e.zip
argaze-1b9957ab30697c57370469b5334e48ce4fe9202e.tar.gz
argaze-1b9957ab30697c57370469b5334e48ce4fe9202e.tar.bz2
argaze-1b9957ab30697c57370469b5334e48ce4fe9202e.tar.xz
Adding utils script to compute gaze metrics.
-rw-r--r--src/argaze/utils/tobii_segment_gaze_metrics_export.py237
1 files changed, 237 insertions, 0 deletions
diff --git a/src/argaze/utils/tobii_segment_gaze_metrics_export.py b/src/argaze/utils/tobii_segment_gaze_metrics_export.py
new file mode 100644
index 0000000..c17fa7a
--- /dev/null
+++ b/src/argaze/utils/tobii_segment_gaze_metrics_export.py
@@ -0,0 +1,237 @@
+#!/usr/bin/env python
+
+import argparse
+import os
+import math
+
+from argaze import DataStructures, GazeFeatures
+from argaze.AreaOfInterest import AOIFeatures
+from argaze.GazeAnalysis import DispersionBasedGazeMovementIdentifier
+from argaze.TobiiGlassesPro2 import TobiiEntities, TobiiVideo, TobiiSpecifications
+from argaze.utils import MiscFeatures
+
+import cv2 as cv
+import numpy
+import pandas
+
+def main():
+ """
+ Analyse fixations and saccades
+ """
+
+ # Manage arguments
+ parser = argparse.ArgumentParser(description=main.__doc__.split('-')[0])
+ parser.add_argument('-s', '--segment_path', metavar='SEGMENT_PATH', type=str, default=None, help='path to a tobii segment folder', required=True)
+ parser.add_argument('-a', '--aoi', metavar='AOI_NAME', type=str, default=None, help='aoi name where to project gaze', required=True)
+ parser.add_argument('-t', '--time_range', metavar=('START_TIME', 'END_TIME'), nargs=2, type=float, default=(0., None), help='start and end time (in second)')
+ parser.add_argument('-p', '--period', metavar=('PERIOD_TIME'), type=float, default=10, help='period of time (in second)')
+ parser.add_argument('-o', '--output', metavar='OUT', type=str, default=None, help='destination folder path (segment folder by default)')
+ args = parser.parse_args()
+
+ # Manage destination path
+ destination_path = '.'
+ if args.output != None:
+
+ if not os.path.exists(os.path.dirname(args.output)):
+
+ os.makedirs(os.path.dirname(args.output))
+ print(f'{os.path.dirname(args.output)} folder created')
+
+ destination_path = args.output
+
+ else:
+
+ destination_path = args.segment_path
+
+ # Export into a dedicated time range folder
+ if args.time_range[1] != None:
+ timerange_path = f'[{int(args.time_range[0])}s - {int(args.time_range[1])}s]'
+ else:
+ timerange_path = f'[all]'
+
+ destination_path = f'{destination_path}/{timerange_path}/{args.aoi}'
+
+ if not os.path.exists(destination_path):
+
+ os.makedirs(destination_path)
+ print(f'{destination_path} folder created')
+
+ fixations_json_filepath = f'{destination_path}/gaze_fixations.json'
+ saccades_json_filepath = f'{destination_path}/gaze_saccades.json'
+ unknown_json_filepath = f'{destination_path}/gaze_unknown.json'
+ gaze_status_json_filepath = f'{destination_path}/gaze_status.json'
+
+ gaze_metrics_filepath = f'{destination_path}/gaze_metrics.csv'
+
+ # Load gaze movements
+ ts_fixations = GazeFeatures.TimeStampedGazeMovements.from_json(fixations_json_filepath)
+ ts_saccades = GazeFeatures.TimeStampedGazeMovements.from_json(saccades_json_filepath)
+ ts_unknown = GazeFeatures.TimeStampedGazeMovements.from_json(unknown_json_filepath)
+ ts_status = GazeFeatures.TimeStampedGazeStatus.from_json(gaze_status_json_filepath)
+
+ print(f'\nLoaded gaze movements count:')
+ print(f'\tFixations: {len(ts_fixations)}')
+ print(f'\tSaccades: {len(ts_saccades)}')
+ print(f'\tUnknown movements: {len(ts_unknown)}')
+
+ # Load tobii segment
+ tobii_segment = TobiiEntities.TobiiSegment(args.segment_path, int(args.time_range[0] * 1e6), int(args.time_range[1] * 1e6) if args.time_range[1] != None else None)
+
+ # Get participant name
+ participant_name = TobiiEntities.TobiiParticipant(f'{args.segment_path}/../../').name
+
+ print(f'\nParticipant: {participant_name}')
+
+ # Load a tobii segment video
+ tobii_segment_video = tobii_segment.load_video()
+ print(f'\nVideo properties:\n\tduration: {tobii_segment_video.duration * 1e-6} s\n\twidth: {tobii_segment_video.width} px\n\theight: {tobii_segment_video.height} px')
+
+ # Prepare gaze metrics
+ ts_metrics = DataStructures.TimeStampedBuffer()
+
+ fixations_exist = len(ts_fixations) > 0
+ saccades_exist = len(ts_saccades) > 0
+ unknown_exist = len(ts_unknown) > 0
+ status_exist = len(ts_status) > 0
+
+ if fixations_exist:
+
+ # Create pandas dataframe
+ fixations_dataframe = ts_fixations.as_dataframe()
+
+ # Reset time range offset
+ fixations_dataframe.index = fixations_dataframe.index - fixations_dataframe.index[0]
+
+ # Add 'end' column
+ fixations_dataframe['end'] = fixations_dataframe.index + fixations_dataframe.duration
+
+ if saccades_exist:
+
+ # Create pandas dataframe
+ saccades_dataframe = ts_saccades.as_dataframe()
+
+ # Reset time range offset
+ saccades_dataframe.index = saccades_dataframe.index - saccades_dataframe.index[0]
+
+ # Add 'end' column
+ saccades_dataframe['end'] = saccades_dataframe.index + saccades_dataframe.duration
+
+ if unknown_exist:
+
+ # Create pandas dataframe
+ unknown_dataframe = ts_unknown.as_dataframe()
+
+ # Reset time range offset
+ unknown_dataframe.index = unknown_dataframe.index - unknown_dataframe.index[0]
+
+ # Add 'end' column
+ unknown_dataframe['end'] = unknown_dataframe.index + unknown_dataframe.duration
+
+ # Work with period of time in microseconds instead of seconds
+ period_duration = args.period * 1e6
+
+ for i in range(0, int(tobii_segment_video.duration/period_duration)):
+
+ period_start_ts = i*period_duration
+ period_end_ts = (i+1)*period_duration
+ period_metrics = {}
+
+ #print(f'\n*** Anaysing period n°{i} [{period_start_ts * 1e-6:.3f}s, {period_end_ts * 1e-6:.3f}s]')
+
+ # Store period duration
+ period_metrics['duration (ms)'] = period_duration * 1e-3
+
+ # Analyse fixations
+ if fixations_exist:
+
+ # Select period
+ fixations_period_dataframe = fixations_dataframe[(fixations_dataframe.index >= period_start_ts) & (fixations_dataframe.end < period_end_ts)]
+
+ if not fixations_period_dataframe.empty:
+
+ #print('\n* Fixations:\n', fixations_period_dataframe)
+
+ fixations_duration_sum = fixations_period_dataframe.duration.sum()
+
+ period_metrics['fixations_number'] = fixations_period_dataframe.shape[0]
+
+ period_metrics['fixations_duration_mean (ms)'] = fixations_period_dataframe.duration.mean() * 1e-3
+ period_metrics['fixations_duration_sum (ms)'] = fixations_duration_sum * 1e-3
+ period_metrics['fixations_duration_ratio (%)'] = fixations_duration_sum / period_duration * 100
+
+ period_metrics['fixations_dispersion_mean (px)'] = fixations_period_dataframe.dispersion.mean()
+
+ else:
+
+ period_metrics['fixations_number'] = 0
+
+ # Analyse saccades
+ if saccades_exist:
+
+ # Select period
+ saccades_period_dataframe = saccades_dataframe[(saccades_dataframe.index >= period_start_ts) & (saccades_dataframe.end < period_end_ts)]
+
+ if not saccades_period_dataframe.empty:
+
+ #print('\n* Saccades:\n', saccades_period_dataframe)
+
+ saccades_duration_sum = saccades_period_dataframe.duration.sum()
+
+ period_metrics['saccades_number'] = saccades_period_dataframe.shape[0]
+
+ period_metrics['saccades_duration_mean (ms)'] = saccades_period_dataframe.duration.mean() * 1e-3
+ period_metrics['saccades_duration_sum (ms)'] = saccades_duration_sum * 1e-3
+ period_metrics['saccades_duration_ratio (%)'] = saccades_duration_sum / period_duration * 100
+
+ period_metrics['saccades_distance_mean (px)'] = saccades_period_dataframe.distance.mean()
+
+ else:
+
+ period_metrics['saccades_number'] = 0
+
+ # Export unknown movements analysis
+ if unknown_exist:
+
+ # Select period
+ unknown_period_dataframe = unknown_dataframe[(unknown_dataframe.index >= period_start_ts) & (unknown_dataframe.end < period_end_ts)]
+
+ if not unknown_period_dataframe.empty:
+
+ #print('\n* Unknown movements:\n', unknown_period_dataframe)
+
+ unknown_duration_sum = unknown_period_dataframe.duration.sum()
+
+ #period_metrics['unknown_number'] = unknown_period_dataframe.shape[0]
+ #period_metrics['unknown_duration_mean (ms)'] = unknown_period_dataframe.duration.mean() * 1e-3
+ #period_metrics['unknown_duration_sum (ms)'] = unknown_duration_sum * 1e-3
+ #period_metrics['unknown_duration_ratio (%)'] = unknown_duration_sum / period_duration * 100
+
+ #else:
+
+ #period_metrics['unknown_number'] = 0
+
+ if fixations_exist and saccades_exist:
+
+ if not fixations_period_dataframe.empty and not saccades_period_dataframe.empty:
+
+ period_metrics['exploit_explore_ratio'] = fixations_duration_sum / saccades_duration_sum
+
+ if unknown_exist and not unknown_period_dataframe.empty:
+
+ period_metrics['unknown_movements_ratio'] = unknown_duration_sum / (fixations_duration_sum + saccades_duration_sum)
+
+ else:
+
+ period_metrics['unknown_movements_ratio'] = 0.0
+
+ # Append period metrics
+ ts_metrics[int(period_start_ts * 1e-3)] = period_metrics
+
+ # Export metrics
+ metrics_dataframe = ts_metrics.as_dataframe() #pandas.DataFrame(metrics, index=[participant_name])
+ metrics_dataframe.to_csv(gaze_metrics_filepath, index=True)
+ print(f'\nGaze metrics saved into {gaze_metrics_filepath}\n')
+
+if __name__ == '__main__':
+
+ main()