From 876a70f9e3ed7066f8ab68ed80ab3fc511d36c22 Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Tue, 16 Jan 2024 11:19:46 +0100 Subject: Adding a new DataStructure SharedObject and using it in ArLayer and ArFrame. --- src/argaze/ArFeatures.py | 83 +++++++++++++--------------------- src/argaze/ArUcoMarkers/ArUcoCamera.py | 10 ++-- src/argaze/DataStructures.py | 22 +++++++++ 3 files changed, 61 insertions(+), 54 deletions(-) diff --git a/src/argaze/ArFeatures.py b/src/argaze/ArFeatures.py index 4e05764..f725380 100644 --- a/src/argaze/ArFeatures.py +++ b/src/argaze/ArFeatures.py @@ -94,10 +94,13 @@ DEFAULT_ARLAYER_DRAW_PARAMETERS = { } @dataclass -class ArLayer(): +class ArLayer(DataStructures.SharedObject): """ Defines a space where to make matching of gaze movements and AOI and inside which those matchings need to be analyzed. + !!! note + Inherits from DataStructures.SharedObject class to be shared by multiple threads + Parameters: name: name of the layer aoi_scene: AOI scene description @@ -118,15 +121,15 @@ class ArLayer(): def __post_init__(self): + # Init sharedObject + super().__init__() + # Define parent attribute: it will be setup by parent later self.__parent = None # Init current gaze movement self.__gaze_movement = GazeFeatures.UnvalidGazeMovement() - # Init lock to share looking data with multiples threads - self.__look_lock = threading.Lock() - # Cast aoi scene to its effective dimension if self.aoi_scene.dimension == 2: @@ -372,7 +375,7 @@ class ArLayer(): """ # Lock layer exploitation - self.__look_lock.acquire() + self.acquire() # Store look execution start date look_start = time.perf_counter() @@ -460,7 +463,7 @@ class ArLayer(): execution_times['total'] = (time.perf_counter() - look_start) * 1e3 # Unlock layer exploitation - self.__look_lock.release() + self.release() # Return look data return looked_aoi, aoi_scan_path_analysis, execution_times, exception @@ -479,8 +482,8 @@ class ArLayer(): return self.draw(image, **self.draw_parameters) - # Lock frame exploitation - self.__look_lock.acquire() + # Lock layer exploitation + self.acquire() # Draw aoi if required if draw_aoi_scene is not None: @@ -492,8 +495,8 @@ class ArLayer(): self.aoi_matcher.draw(image, self.aoi_scene, **draw_aoi_matching) - # Unlock frame exploitation - self.__look_lock.release() + # Unlock layer exploitation + self.release() # Define default ArFrame image parameters DEFAULT_ARFRAME_IMAGE_PARAMETERS = { @@ -517,10 +520,13 @@ DEFAULT_ARFRAME_IMAGE_PARAMETERS = { } @dataclass -class ArFrame(): +class ArFrame(DataStructures.SharedObject): """ Defines a rectangular area where to project in timestamped gaze positions and inside which they need to be analyzed. + !!! note + Inherits from DataStructures.SharedObject class to be shared by multiple threads + Parameters: name: name of the frame size: defines the dimension of the rectangular area where gaze positions are projected @@ -551,6 +557,9 @@ class ArFrame(): def __post_init__(self): + # Init sharedObject + super().__init__() + # Define parent attribute: it will be setup by parent later self.__parent = None @@ -562,9 +571,6 @@ class ArFrame(): # Init current gaze position self.__gaze_position = GazeFeatures.UnvalidGazePosition() - # Init lock to share looked data with multiples threads - self.__look_lock = threading.Lock() - # Prepare logging if needed self.__ts_logs = {} @@ -852,7 +858,7 @@ class ArFrame(): """ # Lock frame exploitation - self.__look_lock.acquire() + self.acquire() # Store look execution start date look_start = time.perf_counter() @@ -988,7 +994,7 @@ class ArFrame(): execution_times['total'] = (time.perf_counter() - look_start) * 1e3 # Unlock frame exploitation - self.__look_lock.release() + self.release() # Return look data return self.__gaze_position, identified_gaze_movement, scan_step_analysis, layer_analysis, execution_times, exception @@ -1009,7 +1015,7 @@ class ArFrame(): """ # Lock frame exploitation - self.__look_lock.acquire() + self.acquire() # Draw background only if background_weight is not None and (heatmap_weight is None or self.heatmap is None): @@ -1066,7 +1072,7 @@ class ArFrame(): self.__gaze_position.draw(image, **draw_gaze_positions) # Unlock frame exploitation - self.__look_lock.release() + self.release() return image @@ -1393,9 +1399,6 @@ class ArCamera(ArFrame): layer.aoi_scan_path.expected_aoi = expected_aoi_list layer.aoi_matcher.exclude = exclude_aoi_list - - # Init a lock to share scene projections into camera frame between multiple threads - self._frame_lock = threading.Lock() def __str__(self) -> str: """ @@ -1470,7 +1473,7 @@ class ArCamera(ArFrame): wait_start = time.perf_counter() waiting_time = 0 - while self._frame_lock.locked(): + while super().locked(): time.sleep(1e-6) waiting_time = (time.perf_counter() - wait_start) * 1e3 @@ -1485,12 +1488,12 @@ class ArCamera(ArFrame): #if waiting_time > 0: # print(f'ArCamera: waiting {waiting_time:.3f} ms before to process gaze position at {timestamp} time.') - # Lock camera frame exploitation - self._frame_lock.acquire() - # Project gaze position into camera frame yield self, super().look(timestamp, gaze_position) + # Lock camera frame exploitation + super().acquire() + # Project gaze position into each scene frames if possible for scene_frame in self.scene_frames: @@ -1517,7 +1520,7 @@ class ArCamera(ArFrame): pass # Unlock camera frame exploitation - self._frame_lock.release() + super().release() def map(self): """Project camera frame background into scene frames background. @@ -1527,11 +1530,11 @@ class ArCamera(ArFrame): """ # Can't use camera frame when it is locked - if self._frame_lock.locked(): + if super().locked(): return # Lock camera frame exploitation - self._frame_lock.acquire() + super().acquire() # Project camera frame background into each scene frame if possible for frame in self.scene_frames: @@ -1555,29 +1558,7 @@ class ArCamera(ArFrame): pass # Unlock camera frame exploitation - self._frame_lock.release() - - def image(self, **kwargs: dict) -> numpy.array: - """ - Get frame image. - - Parameters: - kwargs: ArFrame.image parameters - """ - - # Can't use camera frame when it is locked - if self._frame_lock.locked(): - return - - # Lock camera frame exploitation - self._frame_lock.acquire() - - _image = super().image(**kwargs) - - # Unlock camera frame exploitation - self._frame_lock.release() - - return _image + super().release() def to_json(self, json_filepath): """Save camera to .json file.""" diff --git a/src/argaze/ArUcoMarkers/ArUcoCamera.py b/src/argaze/ArUcoMarkers/ArUcoCamera.py index 75c8bfa..c7df7e0 100644 --- a/src/argaze/ArUcoMarkers/ArUcoCamera.py +++ b/src/argaze/ArUcoMarkers/ArUcoCamera.py @@ -155,7 +155,7 @@ class ArUcoCamera(ArFeatures.ArCamera): detection_time = self.aruco_detector.detect_markers(image) # Lock camera frame exploitation - self._frame_lock.acquire() + super().acquire() # Store projection execution start date projection_start = time.perf_counter() @@ -212,7 +212,7 @@ class ArUcoCamera(ArFeatures.ArCamera): projection_time = (time.perf_counter() - projection_start) * 1e3 # Unlock camera frame exploitation - self._frame_lock.release() + super().release() # Return detection time, projection time and exceptions return detection_time, projection_time, exceptions @@ -228,9 +228,13 @@ class ArUcoCamera(ArFeatures.ArCamera): """ # Get camera frame image - # Note: don't use self._frame_lock here as super().image manage it. + # Note: don't lock/unlock camera frame here as super().image manage it. image = super().image(**kwargs) + # Check image + if image is None: + return + # Draw optic parameters grid if required if draw_optic_parameters_grid is not None: diff --git a/src/argaze/DataStructures.py b/src/argaze/DataStructures.py index fc5072b..d3b6544 100644 --- a/src/argaze/DataStructures.py +++ b/src/argaze/DataStructures.py @@ -13,6 +13,7 @@ import collections import json import ast import bisect +import threading, traceback, sys import pandas import numpy @@ -99,6 +100,27 @@ class JsonEncoder(json.JSONEncoder): return public_dict +class SharedObject(): + """Enable multiple threads sharing.""" + + def __init__(self): + self._lock = threading.Lock() + + def acquire(self): + self._lock.acquire() + + def release(self): + self._lock.release() + + def locked(self) -> bool: + return self._lock.locked() + + def __enter__(self): + self.acquire() + + def __exit__(self, type, value, traceback): + self.release() + class TimeStampedBuffer(collections.OrderedDict): """Ordered dictionary to handle timestamped data. ``` -- cgit v1.1