From 1b9957ab30697c57370469b5334e48ce4fe9202e Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Sat, 10 Dec 2022 18:30:30 +0100 Subject: Adding utils script to compute gaze metrics. --- .../utils/tobii_segment_gaze_metrics_export.py | 237 +++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 src/argaze/utils/tobii_segment_gaze_metrics_export.py 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() -- cgit v1.1