diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/argaze/ArFeatures.py | 96 | ||||
-rw-r--r-- | src/argaze/GazeFeatures.py | 6 | ||||
-rw-r--r-- | src/argaze/utils/demo_gaze_features_run.py | 19 |
3 files changed, 76 insertions, 45 deletions
diff --git a/src/argaze/ArFeatures.py b/src/argaze/ArFeatures.py index b4b1e39..476e413 100644 --- a/src/argaze/ArFeatures.py +++ b/src/argaze/ArFeatures.py @@ -65,14 +65,21 @@ class SceneProjectionFailed(Exception): @dataclass class ArFrame(): """ - Define Augmented Reality frame as an AOI2DScene made from a projected then reframed parent AOI3DScene. + Defines rectangular area where to project in timestamped gaze positions and inside which they need to be analyzed. Parameters: name: name of the frame - size: frame dimension in pixel. + size: defines the dimension of the rectangular area where gaze positions are projected. + aoi_2d_scene: AOI 2D scene description background: image to draw behind - aoi_2d_scene: AOI 2D scene description ... : see [orthogonal_projection][argaze.ArFeatures.ArScene.orthogonal_projection] and [reframe][argaze.AreaOfInterest.AOI2DScene.reframe] functions. - ... + gaze_movement_identifier: gaze movement identification algorithm + current_fixation_matching: enable AOI fixation matching even for in progress fixation + looked_aoi_covering_threshold: + scan_path: scan path object + scan_path_analyzers: dictionary of scan path analysis to apply on scan path + aoi_scan_path: AOI scan path object + aoi_scan_path_analyzers: dictionary of scan path analysis to apply on AOI scan path + heatmap: heatmap object """ name: str @@ -104,6 +111,12 @@ class ArFrame(): @classmethod def from_dict(self, frame_data, working_directory: str = None) -> ArFrameType: + """Load attributes from dictionary. + + Parameters: + frame_data: dictionary with attributes to load + working_directory: folder path where to load files when a dictionary value is a relative filepath. + """ # Load name try: @@ -152,16 +165,20 @@ class ArFrame(): except KeyError: - new_frame_background = numpy.zeros((new_frame_size[1], new_frame_size[0], 3)).astype(numpy.uint8) + new_frame_background = numpy.full((new_frame_size[1], new_frame_size[0], 3), 127).astype(numpy.uint8) # Load gaze movement identifier try: gaze_movement_identifier_value = frame_data.pop('gaze_movement_identifier') - gaze_movement_identifier_type, gaze_movement_identifier_parameters = gaze_movement_identifier_value.popitem() + gaze_movement_identifier_module_path, gaze_movement_identifier_parameters = gaze_movement_identifier_value.popitem() + + # Prepend argaze.GazeAnalysis path when a single name is provided + if len(gaze_movement_identifier_module_path.split('.')) == 1: + gaze_movement_identifier_module_path = f'argaze.GazeAnalysis.{gaze_movement_identifier_module_path}' - gaze_movement_identifier_module = importlib.import_module(f'argaze.GazeAnalysis.{gaze_movement_identifier_type}') + gaze_movement_identifier_module = importlib.import_module(gaze_movement_identifier_module_path) new_gaze_movement_identifier = gaze_movement_identifier_module.GazeMovementIdentifier(**gaze_movement_identifier_parameters) except KeyError: @@ -204,9 +221,15 @@ class ArFrame(): new_scan_path_analyzers_value = frame_data.pop('scan_path_analyzers') - for scan_path_analyzer_type, scan_path_analyzer_parameters in new_scan_path_analyzers_value.items(): + for scan_path_analyzer_module_path, scan_path_analyzer_parameters in new_scan_path_analyzers_value.items(): - scan_path_analyzer_module = importlib.import_module(f'argaze.GazeAnalysis.{scan_path_analyzer_type}') + print(scan_path_analyzer_module_path) + + # Prepend argaze.GazeAnalysis path when a single name is provided + if len(scan_path_analyzer_module_path.split('.')) == 1: + scan_path_analyzer_module_path = f'argaze.GazeAnalysis.{scan_path_analyzer_module_path}' + + scan_path_analyzer_module = importlib.import_module(scan_path_analyzer_module_path) # Check scan path analyzer parameters type members = getmembers(scan_path_analyzer_module.ScanPathAnalyzer) @@ -231,11 +254,11 @@ class ArFrame(): except KeyError: - raise EnvironmentJSONLoadingFailed(f'{scan_path_analyzer_type} scan path analyzer loading fails because {parameter_module_path[2]} scan path analyzer is missing.') + raise EnvironmentJSONLoadingFailed(f'{scan_path_analyzer_module_path} scan path analyzer loading fails because {parameter_module_path[2]} scan path analyzer is missing.') scan_path_analyzer = scan_path_analyzer_module.ScanPathAnalyzer(**scan_path_analyzer_parameters) - new_scan_path_analyzers[scan_path_analyzer_type] = scan_path_analyzer + new_scan_path_analyzers[scan_path_analyzer_module_path] = scan_path_analyzer # Force scan path creation if len(new_scan_path_analyzers) > 0 and new_scan_path == None: @@ -266,9 +289,13 @@ class ArFrame(): new_aoi_scan_path_analyzers_value = frame_data.pop('aoi_scan_path_analyzers') - for aoi_scan_path_analyzer_type, aoi_scan_path_analyzer_parameters in new_aoi_scan_path_analyzers_value.items(): + for aoi_scan_path_analyzer_module_path, aoi_scan_path_analyzer_parameters in new_aoi_scan_path_analyzers_value.items(): + + # Prepend argaze.GazeAnalysis path when a single name is provided + if len(aoi_scan_path_analyzer_module_path.split('.')) == 1: + aoi_scan_path_analyzer_module_path = f'argaze.GazeAnalysis.{aoi_scan_path_analyzer_module_path}' - aoi_scan_path_analyzer_module = importlib.import_module(f'argaze.GazeAnalysis.{aoi_scan_path_analyzer_type}') + aoi_scan_path_analyzer_module = importlib.import_module(aoi_scan_path_analyzer_module_path) # Check aoi scan path analyzer parameters type members = getmembers(aoi_scan_path_analyzer_module.AOIScanPathAnalyzer) @@ -293,11 +320,11 @@ class ArFrame(): except KeyError: - raise EnvironmentJSONLoadingFailed(f'{aoi_scan_path_analyzer_type} aoi scan path analyzer loading fails because {parameter_module_path[2]} aoi scan path analyzer is missing.') + raise EnvironmentJSONLoadingFailed(f'{aoi_scan_path_analyzer_module_path} aoi scan path analyzer loading fails because {parameter_module_path[2]} aoi scan path analyzer is missing.') aoi_scan_path_analyzer = aoi_scan_path_analyzer_module.AOIScanPathAnalyzer(**aoi_scan_path_analyzer_parameters) - new_aoi_scan_path_analyzers[aoi_scan_path_analyzer_type] = aoi_scan_path_analyzer + new_aoi_scan_path_analyzers[aoi_scan_path_analyzer_module_path] = aoi_scan_path_analyzer # Force AOI scan path creation if len(new_aoi_scan_path_analyzers) > 0 and new_aoi_scan_path == None: @@ -343,7 +370,7 @@ class ArFrame(): @classmethod def from_json(self, json_filepath: str) -> ArEnvironmentType: """ - Load ArFrame from .json file. + Load attributes from .json file. Parameters: json_filepath: path to json file @@ -452,15 +479,22 @@ class ArFrame(): # Update looked aoi covering mean self.__looked_aoi_covering_mean = int(100 * max_covering / self.__look_count) / 100 - def look(self, timestamp: int|float, inner_gaze_position: GazeFeatures.GazePosition = GazeFeatures.UnvalidGazePosition(), identified_gaze_movement: GazeFeatures.GazeMovement = GazeFeatures.UnvalidGazeMovement()) -> Tuple[GazeFeatures.GazeMovement, dict, dict, dict]: - """ - - GazeFeatures.AOIScanStepError + def look(self, timestamp: int|float, gaze_position: GazeFeatures.GazePosition = GazeFeatures.UnvalidGazePosition(), identified_gaze_movement: GazeFeatures.GazeMovement = GazeFeatures.UnvalidGazeMovement()) -> Tuple[GazeFeatures.GazeMovement, dict, dict, dict]: + """ + Project gaze position into frame. + + !!! warning + Be aware that gaze positions are in the same range of value than size attribute. + + Parameters: + timestamp: + gaze_position: gaze position to project + identified_gaze_movement: pass identified gaze movement instead of timestamped gaze position to avoid double identification process. Returns: - temp_gaze_movement: identified gaze movement (if gaze_movement_identifier is instanciated) or current gaze movement (if current_fixation_matching is True) - scan_step: new scan step (if scan_path is instanciated) - aoi_scan_step: new scan step (if aoi_scan_path is instanciated) + identified_gaze_movement: identified gaze movement from incoming consecutive timestamped gaze positions if gaze_movement_identifier is instanciated. Current gaze movement if current_fixation_matching is True. + scan_path_analysis: scan path analysis at each new scan step if scan_path is instanciated + aoi_scan_path_analysis: new scan step at each new aoi scan step if aoi_scan_path is instanciated exception: error catched during gaze position processing """ @@ -468,7 +502,7 @@ class ArFrame(): self.__look_lock.acquire() # Update current gaze position - self.__gaze_position = inner_gaze_position + self.__gaze_position = gaze_position # No gaze movement identified by default temp_gaze_movement = GazeFeatures.UnvalidGazeMovement() @@ -535,7 +569,7 @@ class ArFrame(): # Is there a new step? if aoi_scan_step and len(self.aoi_scan_path) > 1: - for aoi_scan_path_analyzer_type, aoi_scan_path_analyzer in self.aoi_scan_path_analyzers.items(): + for aoi_scan_path_analyzer_module_path, aoi_scan_path_analyzer in self.aoi_scan_path_analyzers.items(): # Store aoi scan step analysis start date aoi_scan_step_analysis_start = time.perf_counter() @@ -544,10 +578,10 @@ class ArFrame(): aoi_scan_path_analyzer.analyze(self.aoi_scan_path) # Assess aoi scan step analysis time in ms - times['aoi_scan_step_analyzers'][aoi_scan_path_analyzer_type] = (time.perf_counter() - aoi_scan_step_analysis_start) * 1e3 + times['aoi_scan_step_analyzers'][aoi_scan_path_analyzer_module_path] = (time.perf_counter() - aoi_scan_step_analysis_start) * 1e3 # Store analysis - aoi_scan_step_analysis[aoi_scan_path_analyzer_type] = aoi_scan_path_analyzer.analysis + aoi_scan_step_analysis[aoi_scan_path_analyzer_module_path] = aoi_scan_path_analyzer.analysis elif GazeFeatures.is_saccade(temp_gaze_movement): @@ -562,7 +596,7 @@ class ArFrame(): # Is there a new step? if scan_step and len(self.scan_path) > 1: - for scan_path_analyzer_type, scan_path_analyzer in self.scan_path_analyzers.items(): + for scan_path_analyzer_module_path, scan_path_analyzer in self.scan_path_analyzers.items(): # Store scan step analysis start date scan_step_analysis_start = time.perf_counter() @@ -571,10 +605,10 @@ class ArFrame(): scan_path_analyzer.analyze(self.scan_path) # Assess scan step analysis time in ms - times['scan_step_analyzers'][scan_path_analyzer_type] = (time.perf_counter() - scan_step_analysis_start) * 1e3 + times['scan_step_analyzers'][scan_path_analyzer_module_path] = (time.perf_counter() - scan_step_analysis_start) * 1e3 # Store analysis - scan_step_analysis[scan_path_analyzer_type] = scan_path_analyzer.analysis + scan_step_analysis[scan_path_analyzer_module_path] = scan_path_analyzer.analysis # Append saccade to aoi scan path if self.aoi_scan_path != None: @@ -1280,7 +1314,7 @@ class ArEnvironment(): def look(self, timestamp: int|float, gaze_position: GazeFeatures.GazePosition): """Project timestamped gaze position into each frame. - .. warning:: detect_and_project method needs to be called first. + !!! warning detect_and_project method needs to be called first. """ # Can't use camera frame when it is locked diff --git a/src/argaze/GazeFeatures.py b/src/argaze/GazeFeatures.py index 67e5822..224e2d9 100644 --- a/src/argaze/GazeFeatures.py +++ b/src/argaze/GazeFeatures.py @@ -590,7 +590,11 @@ ScanPathType = TypeVar('ScanPathType', bound="ScanPathType") # Type definition for type annotation convenience class ScanPath(list): - """List of scan steps.""" + """List of scan steps. + + Parameters: + duration_max: duration from which older scan steps are removed each time new scan steps are added. 0 means no maximal duration. + """ def __init__(self, duration_max: int|float = 0): diff --git a/src/argaze/utils/demo_gaze_features_run.py b/src/argaze/utils/demo_gaze_features_run.py index 76d683a..7088639 100644 --- a/src/argaze/utils/demo_gaze_features_run.py +++ b/src/argaze/utils/demo_gaze_features_run.py @@ -10,7 +10,6 @@ __license__ = "BSD" import argparse import os import time -import queue from argaze import ArFeatures, GazeFeatures from argaze.AreaOfInterest import AOIFeatures @@ -50,20 +49,14 @@ def main(): # Update pointer position def on_mouse_event(event, x, y, flags, param): - try: + # Edit millisecond timestamp + timestamp = int((time.time() - start_time) * 1e3) - # Edit millisecond timestamp - timestamp = int((time.time() - start_time) * 1e3) + # Project gaze position into frame + movement, scan_step_analysis, aoi_scan_step_analysis, times, exception = ar_frame.look(timestamp, GazeFeatures.GazePosition((x, y))) - # Project gaze position into frame - movement, scan_step_analysis, aoi_scan_step_analysis, times, exception = ar_frame.look(timestamp, GazeFeatures.GazePosition((x, y))) - - # Do something with look data - # ... - - except GazeFeatures.AOIScanStepError as e: - - print(f'Error on {e.aoi} step:', e) + # Do something with look data + # ... # Attach mouse callback to window cv2.setMouseCallback(ar_frame.name, on_mouse_event) |