From c7ab3eeb9df5540087b5d47018b40d4998abce8f Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Thu, 18 Jan 2024 14:35:35 +0100 Subject: Updating layer look data documentation. --- .../advanced_topics/scripting.md | 5 +- src/argaze/ArFeatures.py | 66 +++++++++------------- src/argaze/DataFeatures.py | 36 +++++++++++- src/argaze/DataLog/File.py | 6 +- .../utils/demo_data/demo_gaze_analysis_setup.json | 6 +- 5 files changed, 73 insertions(+), 46 deletions(-) diff --git a/docs/user_guide/gaze_analysis_pipeline/advanced_topics/scripting.md b/docs/user_guide/gaze_analysis_pipeline/advanced_topics/scripting.md index ab0c6d0..b888b9e 100644 --- a/docs/user_guide/gaze_analysis_pipeline/advanced_topics/scripting.md +++ b/docs/user_guide/gaze_analysis_pipeline/advanced_topics/scripting.md @@ -100,11 +100,14 @@ for name, ar_layer in ar_frame.layers.items(): # Do something with each ArLayer look data for layer_name, layer_look_data in layers_look_data.items(): - looked_aoi_name, aoi_scan_path_analysis, layer_execution_times, layer_exception = layer_look_data + looked_aoi_name, looked_aoi, aoi_scan_path_analysis, layer_execution_times, layer_exception = layer_look_data # Do something with looked AOI name ... + # Do something with looked AOI shape + ... + # Do something with ArLayer AOI scan path analysis for module, analysis in aoi_scan_path_analysis.items(): for data, value in analysis.items(): diff --git a/src/argaze/ArFeatures.py b/src/argaze/ArFeatures.py index f90354a..e7b94c0 100644 --- a/src/argaze/ArFeatures.py +++ b/src/argaze/ArFeatures.py @@ -293,15 +293,7 @@ class ArLayer(DataFeatures.SharedObject): for logger_module_path, logger_parameters in new_loggers_value.items(): - # Prepend argaze.DataLog path when a single name is provided - if len(logger_module_path.split('.')) == 1: - logger_module_path = f'argaze.DataLog.{logger_module_path}' - - logger_module = importlib.import_module(logger_module_path) - - logger = logger_module.TimeStampedDataLogger(**logger_parameters) - - new_loggers[logger_module_path] = logger + new_loggers[logger_module_path] = DataFeatures.TimeStampedDataLogger.from_dict(logger_module_path, logger_parameters) except KeyError: @@ -365,7 +357,8 @@ class ArLayer(DataFeatures.SharedObject): gaze_movement: gaze movement to project Returns: - looked_aoi: most likely looked aoi name + looked_aoi_name: most likely looked aoi name + looked_aoi: most likely looked aoi shape aoi_scan_path_analysis: aoi scan path analysis at each new scan step if aoi_scan_path is instanciated exception: error catched during gaze movement processing """ @@ -446,6 +439,7 @@ class ArLayer(DataFeatures.SharedObject): print('Warning: the following error occurs in ArLayer.look method:', e) + looked_aoi_name = None looked_aoi = None aoi_scan_path_analysis = {} exception = e @@ -453,19 +447,25 @@ class ArLayer(DataFeatures.SharedObject): # Assess total execution time in ms execution_times['total'] = (time.perf_counter() - look_start) * 1e3 - # Edit look data - look_data = looked_aoi_name, aoi_scan_path_analysis, execution_times, exception + # Edit look data dictionary + look_data = { + "looked_aoi_name": looked_aoi_name, + "looked_aoi": looked_aoi, + "aoi_scan_path_analysis": DataFeatures.DataDictionary(aoi_scan_path_analysis), + "execution_times": DataFeatures.DataDictionary(execution_times), + "exception": exception + } # Log look data for logger_module_path, logger in self.loggers.items(): - logger.emit(timestamp, look_data) + logger.emit(timestamp, DataFeatures.DataDictionary(look_data)) # Unlock layer exploitation self.release() - # Return look data - return look_data + # Return look data values + return look_data.values() def draw(self, image: numpy.array, draw_aoi_scene: dict = None, draw_aoi_matching: dict = None): """ @@ -757,15 +757,6 @@ class ArFrame(DataFeatures.SharedObject): pass - # Load log status - try: - - new_frame_log = frame_data.pop('log') - - except KeyError: - - new_frame_log = False - # Load loggers new_loggers = {} @@ -775,15 +766,7 @@ class ArFrame(DataFeatures.SharedObject): for logger_module_path, logger_parameters in new_loggers_value.items(): - # Prepend argaze.DataLog path when a single name is provided - if len(logger_module_path.split('.')) == 1: - logger_module_path = f'argaze.DataLog.{logger_module_path}' - - logger_module = importlib.import_module(logger_module_path) - - logger = logger_module.TimeStampedDataLogger(**logger_parameters) - - new_loggers[logger_module_path] = logger + new_loggers[logger_module_path] = DataFeatures.TimeStampedDataLogger.from_dict(logger_module_path, logger_parameters) except KeyError: @@ -856,9 +839,9 @@ class ArFrame(DataFeatures.SharedObject): current_gaze_position: calibrated gaze position if gaze_position_calibrator is instanciated else, given gaze position. identified_gaze_movement: identified gaze movement from incoming consecutive timestamped gaze positions if gaze_movement_identifier is instanciated. Current gaze movement if filter_in_progress_identification is False. scan_path_analysis: scan path analysis at each new scan step if scan_path is instanciated. - layers_analysis: aoi scan path analysis at each new aoi scan step for each instanciated layers aoi scan path. execution_times: all pipeline steps execution times. exception: error catched during gaze position processing. + layers_look_data: dictionary with each layer's look data. """ # Lock frame exploitation @@ -983,19 +966,26 @@ class ArFrame(DataFeatures.SharedObject): # Assess total execution time in ms execution_times['total'] = (time.perf_counter() - look_start) * 1e3 - # Edit look data - look_data = self.__gaze_position, identified_gaze_movement, scan_step_analysis, execution_times, exception, layers_look_data + # Edit look data dictionary + look_data = { + "gaze_position": self.__gaze_position, + "identified_gaze_movement": identified_gaze_movement, + "scan_step_analysis": DataFeatures.DataDictionary(scan_step_analysis), + "execution_times": DataFeatures.DataDictionary(execution_times), + "exception": exception, + "layers_look_data": DataFeatures.DataDictionary(layers_look_data) + } # Log look data for logger_module_path, logger in self.loggers.items(): - logger.emit(timestamp, look_data) + logger.emit(timestamp, DataFeatures.DataDictionary(look_data)) # Unlock frame exploitation self.release() # Return look data - return look_data + return look_data.values() def __image(self, background_weight: float = None, heatmap_weight: float = None, draw_gaze_position_calibrator: dict = None, draw_scan_path: dict = None, draw_layers: dict = None, draw_gaze_positions: dict = None, draw_fixations: dict = None, draw_saccades: dict = None) -> numpy.array: """ diff --git a/src/argaze/DataFeatures.py b/src/argaze/DataFeatures.py index a52e639..cf7566a 100644 --- a/src/argaze/DataFeatures.py +++ b/src/argaze/DataFeatures.py @@ -8,6 +8,8 @@ __copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" __license__ = "BSD" from typing import TypeVar, Tuple +from dataclasses import dataclass, field +import importlib from inspect import getmembers import collections import json @@ -101,6 +103,13 @@ class JsonEncoder(json.JSONEncoder): return public_dict +class DataDictionary(dict): + """Enable dot.notation access to dictionary attributes""" + + __getattr__ = dict.get + __setattr__ = dict.__setitem__ + __delattr__ = dict.__delitem__ + class SharedObject(): """Enable multiple threads sharing.""" @@ -171,11 +180,34 @@ class SharedObject(): self._token = token self._lock.release() +TimeStampedDataLoggerType = TypeVar('TimeStampedDataLogger', bound="TimeStampedDataLogger") +# Type definition for type annotation convenience + +@dataclass class TimeStampedDataLogger(): """Abstract class to define what should provide a timestamped data logger.""" - def emit(self, timestamp: TimeStampType, data: any): - """Emit timestamped data to a specific log destination.""" + selector: str = field(default='data') + """Code evaluated to select data. Default 'data' string means that all incoming data will be written.""" + + @classmethod + def from_dict(self, logger_module_path: str, logger_parameters: dict) -> TimeStampedDataLoggerType: + """Load timestamped data logger from dictionary. + + Parameters: + logger_module_path: class name to load + logger_parameters: attributes to load + """ + + # Prepend argaze.DataLog path when a single name is provided + if len(logger_module_path.split('.')) == 1: + logger_module_path = f'argaze.DataLog.{logger_module_path}' + + logger_module = importlib.import_module(logger_module_path) + return logger_module.TimeStampedDataLogger(**logger_parameters) + + def emit(self, timestamp: TimeStampType, data: DataDictionary): + """Emit timestamped data dictionary to a specific log destination.""" raise NotImplementedError('emit() method not implemented') diff --git a/src/argaze/DataLog/File.py b/src/argaze/DataLog/File.py index 9253f66..12bad6d 100644 --- a/src/argaze/DataLog/File.py +++ b/src/argaze/DataLog/File.py @@ -12,7 +12,7 @@ from typing import TypeVar, Tuple from dataclasses import dataclass, field import os, pathlib -from argaze import DataFeatures +from argaze import DataFeatures, GazeFeatures @dataclass class TimeStampedDataLogger(DataFeatures.TimeStampedDataLogger): @@ -40,8 +40,8 @@ class TimeStampedDataLogger(DataFeatures.TimeStampedDataLogger): self._file.close() - def emit(self, timestamp: DataFeatures.TimeStampType, data: any): + def emit(self, timestamp: DataFeatures.TimeStampType, data: DataFeatures.DataDictionary): """Write timestamp and data separated by separator char as a new line into file.""" # Write into file - print(f'{timestamp}{self.separator}{data}', file=self._file, flush=True) \ No newline at end of file + print(f'{timestamp}{self.separator}{eval(self.selector)}', file=self._file, flush=True) \ No newline at end of file diff --git a/src/argaze/utils/demo_data/demo_gaze_analysis_setup.json b/src/argaze/utils/demo_data/demo_gaze_analysis_setup.json index 5ae7cf3..38db16f 100644 --- a/src/argaze/utils/demo_data/demo_gaze_analysis_setup.json +++ b/src/argaze/utils/demo_data/demo_gaze_analysis_setup.json @@ -50,7 +50,8 @@ "loggers": { "File" : { "path": "_export/logs/layer_data.txt", - "separator": "," + "separator": ",", + "selector": "data.looked_aoi_name" } } } @@ -58,7 +59,8 @@ "loggers": { "File" : { "path": "_export/logs/frame_data.txt", - "separator": "," + "separator": ",", + "selector": "data.identified_gaze_movement.focus if GazeFeatures.is_fixation(data.identified_gaze_movement) else None" } }, "image_parameters": { -- cgit v1.1