diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/argaze.test/GazeAnalysis/DispersionThresholdIdentification.py | 2 | ||||
-rw-r--r-- | src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py | 2 | ||||
-rw-r--r-- | src/argaze/ArFeatures.py | 13 | ||||
-rw-r--r-- | src/argaze/GazeAnalysis/DeviationCircleCoverage.py | 34 | ||||
-rw-r--r-- | src/argaze/GazeAnalysis/DispersionThresholdIdentification.py | 16 | ||||
-rw-r--r-- | src/argaze/GazeAnalysis/FocusPointInside.py | 16 | ||||
-rw-r--r-- | src/argaze/GazeAnalysis/VelocityThresholdIdentification.py | 15 | ||||
-rw-r--r-- | src/argaze/GazeFeatures.py | 27 | ||||
-rw-r--r-- | src/argaze/utils/demo_data/demo_frame_logger.py | 8 |
9 files changed, 63 insertions, 70 deletions
diff --git a/src/argaze.test/GazeAnalysis/DispersionThresholdIdentification.py b/src/argaze.test/GazeAnalysis/DispersionThresholdIdentification.py index 0bb8ed7..1c54bd1 100644 --- a/src/argaze.test/GazeAnalysis/DispersionThresholdIdentification.py +++ b/src/argaze.test/GazeAnalysis/DispersionThresholdIdentification.py @@ -381,7 +381,7 @@ class TestDispersionThresholdIdentificationClass(unittest.TestCase): # Check that last gaze position date of current fixation is equal to given gaze position date # NOTE: This is not true for saccade as, for I-DT, there is a minimal time window while the gaze movement is unknown - current_gaze_movement = gaze_movement_identifier.current_gaze_movement + current_gaze_movement = gaze_movement_identifier.current_gaze_movement() if current_gaze_movement: if GazeFeatures.is_fixation(current_gaze_movement): diff --git a/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py b/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py index 1c7f7e3..051265c 100644 --- a/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py +++ b/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py @@ -308,7 +308,7 @@ class TestVelocityThresholdIdentificationClass(unittest.TestCase): self.assertNotEqual(finished_gaze_movement[-1].timestamp, gaze_position.timestamp) # Check that last gaze position date of current movement is equal to given gaze position date - current_gaze_movement = gaze_movement_identifier.current_gaze_movement + current_gaze_movement = gaze_movement_identifier.current_gaze_movement() if current_gaze_movement: self.assertEqual(current_gaze_movement[-1].timestamp, gaze_position.timestamp) diff --git a/src/argaze/ArFeatures.py b/src/argaze/ArFeatures.py index b5d1012..b3fb0e2 100644 --- a/src/argaze/ArFeatures.py +++ b/src/argaze/ArFeatures.py @@ -194,7 +194,6 @@ class ArLayer(DataFeatures.SharedObject, DataFeatures.PipelineStepObject): """Get layer's draw parameters dictionary.""" return self.__draw_parameters - @property def last_looked_aoi_name(self) -> str: """Get last looked aoi name.""" return self.__looked_aoi_name @@ -613,12 +612,10 @@ class ArFrame(DataFeatures.SharedObject, DataFeatures.PipelineStepObject): """Get frame's image parameters dictionary.""" return self.__image_parameters - @property def last_gaze_position(self) -> object: """Get last calibrated gaze position""" return self.__calibrated_gaze_position - @property def last_gaze_movement(self) -> object: """Get last identified gaze movement""" return self.__identified_gaze_movement @@ -951,7 +948,7 @@ class ArFrame(DataFeatures.SharedObject, DataFeatures.PipelineStepObject): # No valid finished gaze movement: optionnaly stop in progress identification filtering elif self.__gaze_movement_identifier is not None and not self.__filter_in_progress_identification: - self.__identified_gaze_movement = self.__gaze_movement_identifier.current_gaze_movement + self.__identified_gaze_movement = self.__gaze_movement_identifier.current_gaze_movement() # Update heatmap if self.__heatmap is not None: @@ -1021,16 +1018,16 @@ class ArFrame(DataFeatures.SharedObject, DataFeatures.PipelineStepObject): # Draw current fixation if required if draw_fixations is not None and self.__gaze_movement_identifier is not None: - if self.__gaze_movement_identifier.current_fixation: + if self.__gaze_movement_identifier.current_fixation(): - self.__gaze_movement_identifier.current_fixation.draw(image, **draw_fixations) + self.__gaze_movement_identifier.current_fixation().draw(image, **draw_fixations) # Draw current saccade if required if draw_saccades is not None and self.__gaze_movement_identifier is not None: - if self.__gaze_movement_identifier.current_saccade: + if self.__gaze_movement_identifier.current_saccade(): - self.__gaze_movement_identifier.current_saccade.draw(image, **draw_saccades) + self.__gaze_movement_identifier.current_saccade().draw(image, **draw_saccades) # Draw layers if required if draw_layers is not None: diff --git a/src/argaze/GazeAnalysis/DeviationCircleCoverage.py b/src/argaze/GazeAnalysis/DeviationCircleCoverage.py index c4c5b33..cea3e7a 100644 --- a/src/argaze/GazeAnalysis/DeviationCircleCoverage.py +++ b/src/argaze/GazeAnalysis/DeviationCircleCoverage.py @@ -9,7 +9,6 @@ __copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" __license__ = "BSD" from typing import TypeVar, Tuple -from dataclasses import dataclass, field import math from argaze import GazeFeatures, DataFeatures @@ -21,19 +20,25 @@ import cv2 GazeMovementType = TypeVar('GazeMovement', bound="GazeMovement") # Type definition for type annotation convenience -@dataclass class AOIMatcher(GazeFeatures.AOIMatcher): - """Matching algorithm based on fixation's deviation circle coverage over AOI.""" + """Matching algorithm based on fixation's deviation circle coverage over AOI. - coverage_threshold: float = field(default = 0.) - """Minimal coverage ratio to consider a fixation over an AOI (1 means that whole fixation's deviation circle have to be over the AOI).""" + Parameters: + coverage_threshold: Minimal coverage ratio to consider a fixation over an AOI (1 means that whole fixation's deviation circle have to be over the AOI). + """ + def __init__(self, coverage_threshold: float = 0, **kwargs): - def __post_init__(self): + super().__init__(**kwargs) - super().__init__() + self.__coverage_threshold = coverage_threshold self.__reset() + @property + def coverage_threshold(self): + """Get aoi matcher coverage threshold.""" + return self.__coverage_threshold + def __reset(self): self.__look_count = 0 @@ -60,7 +65,7 @@ class AOIMatcher(GazeFeatures.AOIMatcher): # BAD: we use deviation_max attribute which is an attribute of DispersionThresholdIdentification.Fixation class region, _, circle_ratio = aoi.circle_intersection(gaze_movement.focus, gaze_movement.deviation_max) - if name not in self.exclude and circle_ratio > self.coverage_threshold: + if name not in self.exclude and circle_ratio > self.__coverage_threshold: # Sum circle ratio to update aoi coverage try: @@ -136,13 +141,13 @@ class AOIMatcher(GazeFeatures.AOIMatcher): self.__matched_gaze_movement.draw(image, **draw_matched_fixation) # Draw matched aoi - if self.looked_aoi.all() is not None: + if self.looked_aoi().all() is not None: if update_looked_aoi: try: - self.__looked_aoi_data = (self.looked_aoi_name, aoi_scene[self.looked_aoi_name]) + self.__looked_aoi_data = (self.looked_aoi_name(), aoi_scene[self.looked_aoi_name()]) except KeyError: @@ -151,7 +156,7 @@ class AOIMatcher(GazeFeatures.AOIMatcher): # Draw looked aoi if required if draw_looked_aoi is not None: - self.looked_aoi.draw(image, **draw_looked_aoi) + self.looked_aoi().draw(image, **draw_looked_aoi) # Draw matched region if required if draw_matched_region is not None: @@ -161,22 +166,19 @@ class AOIMatcher(GazeFeatures.AOIMatcher): # Draw looked aoi name if required if looked_aoi_name_color is not None: - top_left_corner_pixel = numpy.rint(self.looked_aoi.bounding_box[0]).astype(int) + looked_aoi_name_offset - cv2.putText(image, self.looked_aoi_name, top_left_corner_pixel, cv2.FONT_HERSHEY_SIMPLEX, 1, looked_aoi_name_color, 1, cv2.LINE_AA) + top_left_corner_pixel = numpy.rint(self.looked_aoi().bounding_box[0]).astype(int) + looked_aoi_name_offset + cv2.putText(image, self.looked_aoi_name(), top_left_corner_pixel, cv2.FONT_HERSHEY_SIMPLEX, 1, looked_aoi_name_color, 1, cv2.LINE_AA) - @property def looked_aoi(self) -> AOIFeatures.AreaOfInterest: """Get most likely looked aoi for current fixation (e.g. the aoi with the highest coverage mean value)""" return self.__looked_aoi_data[1] - @property def looked_aoi_name(self) -> str: """Get most likely looked aoi name for current fixation (e.g. the aoi with the highest coverage mean value)""" return self.__looked_aoi_data[0] - @property def looked_probabilities(self) -> dict: """Get probabilities to be looked by current fixation for each aoi. diff --git a/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py b/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py index 745b62c..08c5fc2 100644 --- a/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py +++ b/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py @@ -145,7 +145,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): # Ignore empty gaze position if not gaze_position: - return GazeFeatures.GazeMovement() if not terminate else self.current_fixation.finish() + return GazeFeatures.GazeMovement() if not terminate else self.current_fixation().finish() # Check if too much time elapsed since last valid gaze position if self.__valid_positions: @@ -155,7 +155,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): if (gaze_position.timestamp - ts_last) > self.__duration_min_threshold: # Get last movement - last_movement = self.current_gaze_movement.finish() + last_movement = self.current_gaze_movement().finish() # Clear all former gaze positions self.__valid_positions = GazeFeatures.TimeStampedGazePositions() @@ -189,7 +189,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): self.__saccade_positions.append(self.__valid_positions[0]) # Finish last saccade - last_saccade = self.current_saccade.finish() + last_saccade = self.current_saccade().finish() # Clear saccade positions self.__saccade_positions = GazeFeatures.TimeStampedGazePositions() @@ -198,7 +198,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): self.__fixation_positions = self.__valid_positions.copy() # Output last saccade - return last_saccade if not terminate else self.current_fixation.finish() + return last_saccade if not terminate else self.current_fixation().finish() # Valid gaze positions deviation too wide else: @@ -212,7 +212,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): self.__saccade_positions.append(self.__fixation_positions[-1]) # Finish last fixation - last_fixation = self.current_fixation.finish() + last_fixation = self.current_fixation().finish() # Clear fixation positions self.__fixation_positions = GazeFeatures.TimeStampedGazePositions() @@ -224,7 +224,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): self.__valid_positions.append(gaze_position) # Output last fixation - return last_fixation if not terminate else self.current_saccade.finish() + return last_fixation if not terminate else self.current_saccade().finish() # Move oldest valid position into saccade positions self.__saccade_positions.append(self.__valid_positions.pop(0)) @@ -232,7 +232,6 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): # Always return empty gaze movement at least return GazeFeatures.GazeMovement() - @property def current_gaze_movement(self) -> GazeMovementType: # It shouldn't have a current fixation and a current saccade at the same time @@ -248,7 +247,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): # Always return empty gaze movement at least return GazeFeatures.GazeMovement() - @property + def current_fixation(self) -> FixationType: if self.__fixation_positions: @@ -258,7 +257,6 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): # Always return empty gaze movement at least return GazeFeatures.GazeMovement() - @property def current_saccade(self) -> SaccadeType: if len(self.__saccade_positions) > 1: diff --git a/src/argaze/GazeAnalysis/FocusPointInside.py b/src/argaze/GazeAnalysis/FocusPointInside.py index 0358fae..56fee76 100644 --- a/src/argaze/GazeAnalysis/FocusPointInside.py +++ b/src/argaze/GazeAnalysis/FocusPointInside.py @@ -9,7 +9,6 @@ __copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" __license__ = "BSD" from typing import TypeVar, Tuple -from dataclasses import dataclass, field import math from argaze import GazeFeatures, DataFeatures @@ -21,13 +20,12 @@ import cv2 GazeMovementType = TypeVar('GazeMovement', bound="GazeMovement") # Type definition for type annotation convenience -@dataclass class AOIMatcher(GazeFeatures.AOIMatcher): """Matching algorithm based on fixation's focus point.""" - def __post_init__(self): + def __init__(self, **kwargs): - super().__init__() + super().__init__(**kwargs) self.__reset() @@ -82,26 +80,24 @@ class AOIMatcher(GazeFeatures.AOIMatcher): self.__matched_gaze_movement.draw(image, **draw_matched_fixation) # Draw matched aoi - if self.looked_aoi.all() is not None: + if self.looked_aoi().all() is not None: # Draw looked aoi if required if draw_looked_aoi is not None: - self.looked_aoi.draw(image, **draw_looked_aoi) + self.looked_aoi().draw(image, **draw_looked_aoi) # Draw looked aoi name if required if looked_aoi_name_color is not None: - top_left_corner_pixel = numpy.rint(self.looked_aoi.bounding_box[0]).astype(int) + looked_aoi_name_offset - cv2.putText(image, self.looked_aoi_name, top_left_corner_pixel, cv2.FONT_HERSHEY_SIMPLEX, 1, looked_aoi_name_color, 1, cv2.LINE_AA) + top_left_corner_pixel = numpy.rint(self.looked_aoi().bounding_box[0]).astype(int) + looked_aoi_name_offset + cv2.putText(image, self.looked_aoi_name(), top_left_corner_pixel, cv2.FONT_HERSHEY_SIMPLEX, 1, looked_aoi_name_color, 1, cv2.LINE_AA) - @property def looked_aoi(self) -> AOIFeatures.AreaOfInterest: """Get most likely looked aoi for current fixation.""" return self.__looked_aoi_data[1] - @property def looked_aoi_name(self) -> str: """Get most likely looked aoi name for current fixation.""" diff --git a/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py b/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py index bfe04fa..64fd3d0 100644 --- a/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py +++ b/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py @@ -145,7 +145,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): # Ignore empty gaze position if not gaze_position: - return GazeFeatures.GazeMovement() if not terminate else self.current_fixation.finish() + return GazeFeatures.GazeMovement() if not terminate else self.current_fixation().finish() # Store first valid position if self.__last_ts < 0: @@ -163,7 +163,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): self.__last_position = gaze_position # Get last movement - last_movement = self.current_gaze_movement.finish() + last_movement = self.current_gaze_movement().finish() # Clear all former gaze positions self.__fixation_positions = GazeFeatures.TimeStampedGazePositions() @@ -191,7 +191,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): self.__saccade_positions.append(self.__fixation_positions[-1]) # Create last fixation - last_fixation = self.current_fixation.finish() + last_fixation = self.current_fixation().finish() # Clear fixation positions self.__fixation_positions = GazeFeatures.TimeStampedGazePositions() @@ -200,7 +200,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): self.__saccade_positions.append(gaze_position) # Output last fixation - return last_fixation if not terminate else self.current_saccade.finish() + return last_fixation if not terminate else self.current_saccade().finish() # Velocity is less or equals to threshold else: @@ -214,7 +214,7 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): self.__fixation_positions.append(self.__saccade_positions[-1]) # Create last saccade - last_saccade = self.current_saccade.finish() + last_saccade = self.current_saccade().finish() # Clear fixation positions self.__saccade_positions = GazeFeatures.TimeStampedGazePositions() @@ -223,12 +223,11 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): self.__fixation_positions.append(gaze_position) # Output last saccade - return last_saccade if not terminate else self.current_fixation.finish() + return last_saccade if not terminate else self.current_fixation().finish() # Always return empty gaze movement at least return GazeFeatures.GazeMovement() - @property def current_gaze_movement(self) -> GazeMovementType: # It shouldn't have a current fixation and a current saccade at the same time @@ -245,7 +244,6 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): # Always return empty gaze movement at least return GazeFeatures.GazeMovement() - @property def current_fixation(self) -> FixationType: if self.__fixation_positions: @@ -255,7 +253,6 @@ class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): # Always return empty gaze movement at least return GazeFeatures.GazeMovement() - @property def current_saccade(self) -> SaccadeType: if len(self.__saccade_positions) > 1: diff --git a/src/argaze/GazeFeatures.py b/src/argaze/GazeFeatures.py index cdd29a3..ac9b69d 100644 --- a/src/argaze/GazeFeatures.py +++ b/src/argaze/GazeFeatures.py @@ -577,19 +577,16 @@ class GazeMovementIdentifier(DataFeatures.PipelineStepObject): raise NotImplementedError('identify() method not implemented') - @property def current_gaze_movement(self) -> GazeMovementType: """Get the current identified gaze movement (finished or in progress) if it exists otherwise, an empty gaze movement.""" raise NotImplementedError('current_gaze_movement getter not implemented') - @property def current_fixation(self) -> FixationType: """Get the current identified fixation (finished or in progress) if it exists otherwise, an empty gaze movement.""" raise NotImplementedError('current_fixation getter not implemented') - @property def current_saccade(self) -> SaccadeType: """Get the current identified saccade (finished or in progress) if it exists otherwise, an empty gaze movement.""" @@ -857,16 +854,25 @@ class ScanPathAnalyzer(DataFeatures.PipelineStepObject): raise NotImplementedError('analyze() method not implemented') -@dataclass class AOIMatcher(DataFeatures.PipelineStepObject): """Abstract class to define what should provide an AOI matcher algorithm.""" - exclude: list[str] = field(default_factory = list) + def __init__(self, exclude: list[str] = [], **kwargs): - def __init__(self): + super().__init__(**kwargs) - super().__init__() + self.__exclude = exclude + @property + def exclude(self): + """Get list of AOI to exclude from matching.""" + return self.__exclude + + @exclude.setter + def exclude(self, exclude: list[str]): + """Set list of AOI to exclude from matching.""" + self.__exclude = exclude + def match(self, aoi_scene: AOIFeatures.AOIScene, gaze_movement: GazeMovement) -> Tuple[str, AOIFeatures.AreaOfInterest]: """Which AOI is looked in the scene?""" @@ -882,17 +888,14 @@ class AOIMatcher(DataFeatures.PipelineStepObject): raise NotImplementedError('draw() method not implemented') - @property def looked_aoi(self) -> AOIFeatures.AreaOfInterest: """Get most likely looked aoi.""" - raise NotImplementedError('looked_aoi getter not implemented') + raise NotImplementedError('looked_aoi() method not implemented') - @property def looked_aoi_name(self) -> str: """Get most likely looked aoi name.""" - - raise NotImplementedError('looked_aoi_name getter not implemented') + raise NotImplementedError('looked_aoi_name() method not implemented') AOIScanStepType = TypeVar('AOIScanStep', bound="AOIScanStep") # Type definition for type annotation convenience diff --git a/src/argaze/utils/demo_data/demo_frame_logger.py b/src/argaze/utils/demo_data/demo_frame_logger.py index 2bb4bdc..151f57b 100644 --- a/src/argaze/utils/demo_data/demo_frame_logger.py +++ b/src/argaze/utils/demo_data/demo_frame_logger.py @@ -16,13 +16,13 @@ class FixationLogger(DataFeatures.PipelineStepObserver, UtilsFeatures.FileWriter """Log fixations.""" # Log fixations - if GazeFeatures.is_fixation(frame.last_gaze_movement) and frame.last_gaze_movement.is_finished(): + if GazeFeatures.is_fixation(frame.last_gaze_movement()) and frame.last_gaze_movement().is_finished(): log = ( timestamp, - frame.last_gaze_movement.focus, - frame.last_gaze_movement.duration, - frame.layers['demo_layer'].last_looked_aoi_name + frame.last_gaze_movement().focus, + frame.last_gaze_movement().duration, + frame.layers['demo_layer'].last_looked_aoi_name() ) self.write(log) |