From 8fc18a434da400f0fe82707e23838d6cc40a787d Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Wed, 3 Jul 2024 17:14:43 +0200 Subject: Rewriting eye tracking context and gaze analysis sections. --- src/argaze/ArFeatures.py | 4 +- src/argaze/utils/contexts/OpenCV.py | 66 +++++++++++------------- src/argaze/utils/contexts/Random.py | 25 ++++++++- src/argaze/utils/demo/opencv_cursor_context.json | 6 +++ src/argaze/utils/demo/opencv_window_context.json | 6 --- 5 files changed, 61 insertions(+), 46 deletions(-) create mode 100644 src/argaze/utils/demo/opencv_cursor_context.json delete mode 100644 src/argaze/utils/demo/opencv_window_context.json (limited to 'src') diff --git a/src/argaze/ArFeatures.py b/src/argaze/ArFeatures.py index aaac6ed..8d9eceb 100644 --- a/src/argaze/ArFeatures.py +++ b/src/argaze/ArFeatures.py @@ -1231,7 +1231,7 @@ class ArCamera(ArFrame): self.__projection_cache_writer.write( (timestamp, exception) ) - def _read_projection_cache(self, timestamp: int|float): + def _read_projection_cache(self, timestamp: int|float) -> bool: """Read layers aoi scene from the projection cache. Parameters: @@ -1504,7 +1504,7 @@ DEFAULT_ARCONTEXT_IMAGE_PARAMETERS = { class ArContext(DataFeatures.PipelineStepObject): """ - Defines abstract Python context manager to handle incoming gaze data before passing them to a processing pipeline. + Defines abstract Python context manager to handle eye tracker data before passing them to a processing pipeline. """ # noinspection PyMissingConstructor diff --git a/src/argaze/utils/contexts/OpenCV.py b/src/argaze/utils/contexts/OpenCV.py index 111ed8e..273705a 100644 --- a/src/argaze/utils/contexts/OpenCV.py +++ b/src/argaze/utils/contexts/OpenCV.py @@ -26,7 +26,12 @@ import cv2 from argaze import ArFeatures, DataFeatures -class Window(ArFeatures.LiveProcessingContext): +class Cursor(ArFeatures.ArContext): + """Process cursor position over OpenCV window. + + !!! warning + It is assumed that an OpenCV window with the same name than the context is used to display context's pipeline image. + """ @DataFeatures.PipelineStepInit def __init__(self, **kwargs): @@ -37,13 +42,13 @@ class Window(ArFeatures.LiveProcessingContext): @DataFeatures.PipelineStepEnter def __enter__(self): - logging.info('OpenCV window context starts...') + logging.info('OpenCV.Cursor context starts...') - # Create a window to display context + # Create a window cv2.namedWindow(self.name, cv2.WINDOW_AUTOSIZE) # Init timestamp - self.__start_time = time.time() + self._start_time = time.time() # Attach mouse event callback to window cv2.setMouseCallback(self.name, self.__on_mouse_event) @@ -53,7 +58,7 @@ class Window(ArFeatures.LiveProcessingContext): @DataFeatures.PipelineStepExit def __exit__(self, exception_type, exception_value, exception_traceback): - logging.info('OpenCV window context stops...') + logging.info('OpenCV.Cursor context stops...') # Delete window cv2.destroyAllWindows() @@ -61,20 +66,24 @@ class Window(ArFeatures.LiveProcessingContext): def __on_mouse_event(self, event, x, y, flags, param): """Process pointer position.""" - logging.debug('Window.on_mouse_event %i %i', x, y) + logging.debug('OpenCV.Cursor.on_mouse_event %i %i', x, y) if not self.is_paused(): # Process timestamped gaze position - self._process_gaze_position(timestamp = int((time.time() - self.__start_time) * 1e3), x = x, y = y) + self._process_gaze_position(timestamp = int((time.time() - self._start_time) * 1e3), x = x, y = y) -class Movie(ArFeatures.PostProcessingContext): +class Movie(Cursor): + """Process movie images and cursor position over OpenCV window. + !!! warning + It is assumed that an OpenCV window with the same name than the context is used to display context's pipeline image. + """ @DataFeatures.PipelineStepInit def __init__(self, **kwargs): - # Init PostProcessingContext class + # Init Cursor class super().__init__() # Init private attributes @@ -109,16 +118,10 @@ class Movie(ArFeatures.PostProcessingContext): @DataFeatures.PipelineStepEnter def __enter__(self): - logging.info('OpenCV movie context starts...') - - # Create a window to display context - cv2.namedWindow(self.name, cv2.WINDOW_AUTOSIZE) - - # Init timestamp - self.__start_time = time.time() + logging.info('OpenCV.Movie context starts...') - # Attach mouse event callback to window - cv2.setMouseCallback(self.name, self.__on_mouse_event) + # Enter in Cursor context + super().__enter__() # Open reading thread self.__reading_thread = threading.Thread(target=self.__read) @@ -174,33 +177,23 @@ class Movie(ArFeatures.PostProcessingContext): @DataFeatures.PipelineStepExit def __exit__(self, exception_type, exception_value, exception_traceback): - logging.info('OpenCV movie context stops...') + logging.info('OpenCV.Movie context stops...') + + # Exit from Cursor context + super().__exit__(exception_type, exception_value, exception_traceback) # Close data stream - self._stop_event.set() + self.stop() # Stop reading thread threading.Thread.join(self.__reading_thread) - # Delete window - cv2.destroyAllWindows() - - def __on_mouse_event(self, event, x, y, flags, param): - """Process pointer position.""" - - logging.debug('Window.on_mouse_event %i %i', x, y) - - if not self.is_paused(): - - # Process timestamped gaze position - self._process_gaze_position(timestamp = int((time.time() - self.__start_time) * 1e3), x = x, y = y) - def refresh(self): """Refresh current frame.""" self.__refresh = True def previous(self): - + """Go to previous frame.""" self.__next_image_index -= 1 # Clip image index @@ -208,6 +201,7 @@ class Movie(ArFeatures.PostProcessingContext): self.__next_image_index = 0 def next(self): + """Go to next frame.""" self.__next_image_index += 1 @@ -217,13 +211,13 @@ class Movie(ArFeatures.PostProcessingContext): @property def duration(self) -> int|float: - """Get data duration.""" + """Get movie duration.""" return self.__movie_length / self.__movie_fps @property def progression(self) -> float: - """Get data processing progression between 0 and 1.""" + """Get movie processing progression between 0 and 1.""" if self.__current_image_index is not None: diff --git a/src/argaze/utils/contexts/Random.py b/src/argaze/utils/contexts/Random.py index 29b9830..c7b2187 100644 --- a/src/argaze/utils/contexts/Random.py +++ b/src/argaze/utils/contexts/Random.py @@ -71,8 +71,29 @@ class GazePositionGenerator(ArFeatures.ArContext): # Edit millisecond timestamp timestamp = int((time.time() - start_time) * 1e3) - self.__x += random.randint(-10, 10) - self.__y += random.randint(-10, 10) + # Random saccade + if random.randint(0, 100) == 0: + + rand_x = random.randint(0, int(self.__range[0] / 2)) + rand_y = random.randint(0, int(self.__range[1] / 2)) + + self.__x += random.randint(-rand_x, rand_x) + self.__y += random.randint(-rand_y, rand_y) + + # Random fixation + else: + + self.__x += random.randint(-1, 1) + self.__y += random.randint(-1, 1) + + # Clip position + if self.__x < 0 or self.__x > self.__range[0]: + + self.__x = int(self.range[0] / 2) + + if self.__y < 0 or self.__y > self.__range[1]: + + self.__y = int(self.range[1] / 2) logging.debug('> timestamp=%i, x=%i, y=%i', timestamp, self.__x, self.__y) diff --git a/src/argaze/utils/demo/opencv_cursor_context.json b/src/argaze/utils/demo/opencv_cursor_context.json new file mode 100644 index 0000000..659ffd6 --- /dev/null +++ b/src/argaze/utils/demo/opencv_cursor_context.json @@ -0,0 +1,6 @@ +{ + "argaze.utils.contexts.OpenCV.Cursor" : { + "name": "OpenCV cursor", + "pipeline": "gaze_analysis_pipeline.json" + } +} \ No newline at end of file diff --git a/src/argaze/utils/demo/opencv_window_context.json b/src/argaze/utils/demo/opencv_window_context.json deleted file mode 100644 index d589665..0000000 --- a/src/argaze/utils/demo/opencv_window_context.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "argaze.utils.contexts.OpenCV.Window" : { - "name": "OpenCV Window", - "pipeline": "gaze_analysis_pipeline.json" - } -} \ No newline at end of file -- cgit v1.1