From 846b5bf301b79e6725c32114532eaf50e1379a81 Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Mon, 22 May 2023 11:33:22 +0200 Subject: Adding fixation, saccade and scan path drawing features. --- .../DispersionThresholdIdentification.py | 15 +++++++ .../VelocityThresholdIdentification.py | 15 +++++++ src/argaze/GazeFeatures.py | 46 ++++++++++++++++++++-- src/argaze/utils/demo_gaze_features_run.py | 35 +++++----------- 4 files changed, 83 insertions(+), 28 deletions(-) diff --git a/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py b/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py index 3219c56..18c9c49 100644 --- a/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py +++ b/src/argaze/GazeAnalysis/DispersionThresholdIdentification.py @@ -14,6 +14,7 @@ import math from argaze import GazeFeatures import numpy +import cv2 FixationType = TypeVar('Fixation', bound="Fixation") # Type definition for type annotation convenience @@ -68,6 +69,12 @@ class Fixation(GazeFeatures.Fixation): return self + def draw(self, frame, color=(127, 127, 127), border_color=(255, 255, 255)): + """Draw fixation into frame.""" + + cv2.circle(frame, (int(self.focus[0]), int(self.focus[1])), int(self.deviation_max), color, -1) + cv2.circle(frame, (int(self.focus[0]), int(self.focus[1])), int(self.deviation_max), border_color, len(self.positions)) + @dataclass(frozen=True) class Saccade(GazeFeatures.Saccade): """Define dispersion based saccade.""" @@ -75,6 +82,14 @@ class Saccade(GazeFeatures.Saccade): def __post_init__(self): super().__post_init__() + def draw(self, frame, color=(255, 255, 255)): + """Draw saccade into frame.""" + + _, start_position = self.positions.first + _, last_position = self.positions.last + + cv2.line(frame, (int(start_position[0]), int(start_position[1])), (int(last_position[0]), int(last_position[1])), color, 2) + @dataclass class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): """Implementation of the I-DT algorithm as described in: diff --git a/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py b/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py index 4aa6384..3c3d58f 100644 --- a/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py +++ b/src/argaze/GazeAnalysis/VelocityThresholdIdentification.py @@ -14,6 +14,7 @@ import math from argaze import GazeFeatures import numpy +import cv2 FixationType = TypeVar('Fixation', bound="Fixation") # Type definition for type annotation convenience @@ -68,6 +69,12 @@ class Fixation(GazeFeatures.Fixation): return self + def draw(self, frame, color=(127, 127, 127), border_color=(255, 255, 255)): + """Draw fixation into frame.""" + + cv2.circle(frame, (int(self.focus[0]), int(self.focus[1])), int(self.deviation_max), color, -1) + cv2.circle(frame, (int(self.focus[0]), int(self.focus[1])), int(self.deviation_max), border_color, len(self.positions)) + @dataclass(frozen=True) class Saccade(GazeFeatures.Saccade): """Define dispersion based saccade.""" @@ -75,6 +82,14 @@ class Saccade(GazeFeatures.Saccade): def __post_init__(self): super().__post_init__() + def draw(self, frame, color=(255, 255, 255)): + """Draw saccade into frame.""" + + _, start_position = self.positions.first + _, last_position = self.positions.last + + cv2.line(frame, (int(start_position[0]), int(start_position[1])), (int(last_position[0]), int(last_position[1])), color, 2) + @dataclass class GazeMovementIdentifier(GazeFeatures.GazeMovementIdentifier): """Implementation of the I-VT algorithm as described in: diff --git a/src/argaze/GazeFeatures.py b/src/argaze/GazeFeatures.py index bfa6303..3592b13 100644 --- a/src/argaze/GazeFeatures.py +++ b/src/argaze/GazeFeatures.py @@ -24,7 +24,7 @@ from argaze import DataStructures import numpy import pandas -import cv2 as cv +import cv2 @dataclass(frozen=True) class GazePosition(): @@ -96,11 +96,11 @@ class GazePosition(): int_value = (int(self.value[0]), int(self.value[1])) # Draw point at position - cv.circle(frame, int_value, 2, color, -1) + cv2.circle(frame, int_value, 2, color, -1) # Draw precision circle if self.precision > 0 and draw_precision: - cv.circle(frame, int_value, round(self.precision), color, 1) + cv2.circle(frame, int_value, round(self.precision), color, 1) class UnvalidGazePosition(GazePosition): """Unvalid gaze position.""" @@ -190,6 +190,22 @@ class GazeMovement(): return output + def draw_positions(self, frame, color=(0, 55, 55)): + """Draw gaze movement positions""" + + gaze_positions = self.positions.copy() + + while len(gaze_positions) >= 2: + + ts_start, start_gaze_position = gaze_positions.pop_first() + ts_next, next_gaze_position = gaze_positions.first + + # Draw start gaze + start_gaze_position.draw(frame, draw_precision=False) + + # Draw movement from start to next + cv2.line(frame, start_gaze_position, next_gaze_position, color, 1) + FixationType = TypeVar('Fixation', bound="Fixation") # Type definition for type annotation convenience @@ -208,6 +224,11 @@ class Fixation(GazeMovement): raise NotImplementedError('merge() method not implemented') + def draw(self, frame, color): + """Draw fixation into frame.""" + + raise NotImplementedError('draw() method not implemented') + def is_fixation(gaze_movement): """Is a gaze movement a fixation?""" @@ -220,6 +241,11 @@ class Saccade(GazeMovement): super().__post_init__() + def draw(self, frame, color): + """Draw saccade into frame.""" + + raise NotImplementedError('draw() method not implemented') + def is_saccade(gaze_movement): """Is a gaze movement a saccade?""" @@ -431,6 +457,20 @@ class ScanPath(list): print(e) + def draw(self, frame, fixation_color=(255, 255, 255), saccade_color=(255, 255, 255), deepness=0): + """Draw scan path into frame.""" + + last_step = None + for step in self[-deepness:]: + + if last_step != None: + + cv2.line(frame, (int(last_step.first_fixation.focus[0]), int(last_step.first_fixation.focus[1])), (int(step.first_fixation.focus[0]), int(step.first_fixation.focus[1])), saccade_color, 2) + + last_step.first_fixation.draw(frame, fixation_color) + + last_step = step + class ScanPathAnalyzer(): """Abstract class to define what should provide a scan path analyzer.""" diff --git a/src/argaze/utils/demo_gaze_features_run.py b/src/argaze/utils/demo_gaze_features_run.py index a957ce0..6e7e01e 100644 --- a/src/argaze/utils/demo_gaze_features_run.py +++ b/src/argaze/utils/demo_gaze_features_run.py @@ -231,20 +231,10 @@ def main(): aoi_scene_projection.draw_circlecast(aoi_matrix, current_fixation.focus, current_fixation.deviation_max, base_color=(0, 0, 0), matching_color=(255, 255, 255)) # Draw current fixation - cv2.circle(aoi_matrix, (int(current_fixation.focus[0]), int(current_fixation.focus[1])), int(current_fixation.deviation_max), (255, 255, 255), len(current_fixation.positions)) - - # Draw current fixation gaze positions - gaze_positions = current_fixation.positions.copy() - while len(gaze_positions) >= 2: - - ts_start, start_gaze_position = gaze_positions.pop_first() - ts_next, next_gaze_position = gaze_positions.first - - # Draw start gaze - start_gaze_position.draw(aoi_matrix, draw_precision=False) + current_fixation.draw(aoi_matrix, color=(255, 255, 0)) - # Draw movement from start to next - cv2.line(aoi_matrix, start_gaze_position, next_gaze_position, (0, 55, 55), 1) + # Draw current fixation gaze positions + current_fixation.draw_positions(aoi_matrix) else: @@ -260,22 +250,17 @@ def main(): current_saccade = gaze_movement_identifier[identification_mode].current_saccade # Draw current saccade gaze positions - gaze_positions = current_saccade.positions.copy() - while len(gaze_positions) >= 2: - - ts_start, start_gaze_position = gaze_positions.pop_first() - ts_next, next_gaze_position = gaze_positions.first + current_saccade.draw_positions(aoi_matrix) - # Draw start gaze - start_gaze_position.draw(aoi_matrix, draw_precision=False) - - # Draw movement from start to next - cv2.line(aoi_matrix, start_gaze_position, next_gaze_position, (0, 0, 255), 1) + # Draw last 10 steps of raw scan path + raw_scan_path.draw(aoi_matrix, fixation_color=(255, 0, 255), deepness=10) # Write last 5 steps of aoi scan path path = '' for step in aoi_scan_path[-5:]: + path += f'> {step.aoi} ' + path += f'> {aoi_scan_path.current_aoi}' cv2.putText(aoi_matrix, path, (20, window_size[1]-40), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 1, cv2.LINE_AA) @@ -326,8 +311,8 @@ def main(): key_pressed = cv2.waitKey(10) - if key_pressed != -1: - print(key_pressed) + #if key_pressed != -1: + # print(key_pressed) # Switch identification mode with 'm' key if key_pressed == 109: -- cgit v1.1