From f0962a6cfe047ac449eafc128cf527c5308310bc Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Wed, 30 Nov 2022 08:39:56 +0100 Subject: Renaming ArGazeScene into ArScene. Replacing validity notion by consistency notion. --- src/argaze.test/ArGazeScene.py | 59 ----- src/argaze.test/ArScene.py | 59 +++++ src/argaze/ArGazeScene.py | 148 ------------- src/argaze/ArScene.py | 148 +++++++++++++ src/argaze/ArUcoMarkers/ArUcoScene.py | 172 +++++++-------- src/argaze/__init__.py | 2 +- .../utils/tobii_segment_argaze_scene_export.py | 235 -------------------- src/argaze/utils/tobii_segment_arscene_export.py | 245 +++++++++++++++++++++ 8 files changed, 539 insertions(+), 529 deletions(-) delete mode 100644 src/argaze.test/ArGazeScene.py create mode 100644 src/argaze.test/ArScene.py delete mode 100644 src/argaze/ArGazeScene.py create mode 100644 src/argaze/ArScene.py delete mode 100644 src/argaze/utils/tobii_segment_argaze_scene_export.py create mode 100644 src/argaze/utils/tobii_segment_arscene_export.py (limited to 'src') diff --git a/src/argaze.test/ArGazeScene.py b/src/argaze.test/ArGazeScene.py deleted file mode 100644 index 4d0b3ae..0000000 --- a/src/argaze.test/ArGazeScene.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python - -import unittest -import os - -from argaze import ArGazeScene - -import numpy - -class TestArGazeSceneClass(unittest.TestCase): - """Test ArGazeScene class.""" - - def test_from_json(self): - """Test ArGazeScene creation from json file.""" - - # Edit scene file path - current_directory = os.path.dirname(os.path.abspath(__file__)) - json_filepath = os.path.join(current_directory, 'utils/scene.json') - - # Load scene - argaze_scene = ArGazeScene.ArGazeScene.from_json(json_filepath) - - # Check scene meta data - self.assertEqual(argaze_scene.name, "TestScene") - self.assertEqual(argaze_scene.aruco_dictionary.name, "DICT_ARUCO_ORIGINAL") - self.assertEqual(argaze_scene.aruco_marker_size, 3.0) - - # Check ArUco camera - self.assertEqual(argaze_scene.aruco_camera.rms, 1.0) - self.assertIsNone(numpy.testing.assert_array_equal(argaze_scene.aruco_camera.dimensions, [1920, 1080])) - self.assertIsNone(numpy.testing.assert_array_equal(argaze_scene.aruco_camera.K, [[1.0, 0.0, 1.0], [0.0, 1.0, 1.0], [0.0, 0.0, 1.0]])) - self.assertIsNone(numpy.testing.assert_array_equal(argaze_scene.aruco_camera.D, [-1.0, -0.5, 0.0, 0.5, 1.0])) - - # Check ArUco tracker - self.assertEqual(argaze_scene.aruco_tracker.tracking_data.cornerRefinementMethod, 3) - self.assertEqual(argaze_scene.aruco_tracker.tracking_data.aprilTagQuadSigma, 2) - self.assertEqual(argaze_scene.aruco_tracker.tracking_data.aprilTagDeglitch, 1) - - # Check ArUco scene - self.assertEqual(argaze_scene.aruco_scene.angle_tolerance, 1.0) - self.assertEqual(argaze_scene.aruco_scene.distance_tolerance, 2.0) - self.assertEqual(len(argaze_scene.aruco_scene.places), 2) - - # Check ArUco scene places - self.assertIsNone(numpy.testing.assert_array_equal(argaze_scene.aruco_scene.places['A'].translation, [1, 0, 0])) - self.assertIsNone(numpy.testing.assert_array_equal(argaze_scene.aruco_scene.places['A'].rotation, [0, 0, 0])) - self.assertEqual(argaze_scene.aruco_scene.places['A'].marker.identifier, 0) - - self.assertIsNone(numpy.testing.assert_array_equal(argaze_scene.aruco_scene.places['B'].translation, [0, 1, 0])) - self.assertIsNone(numpy.testing.assert_array_equal(argaze_scene.aruco_scene.places['B'].rotation, [0, 90, 0])) - self.assertEqual(argaze_scene.aruco_scene.places['B'].marker.identifier, 1) - - # Check AOI scene - self.assertEqual(len(argaze_scene.aoi_scene.items()), 1) - self.assertEqual(argaze_scene.aoi_scene['Test'].size, 4) - -if __name__ == '__main__': - - unittest.main() \ No newline at end of file diff --git a/src/argaze.test/ArScene.py b/src/argaze.test/ArScene.py new file mode 100644 index 0000000..cfb949b --- /dev/null +++ b/src/argaze.test/ArScene.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python + +import unittest +import os + +from argaze import ArScene + +import numpy + +class TestArSceneClass(unittest.TestCase): + """Test ArScene class.""" + + def test_from_json(self): + """Test ArScene creation from json file.""" + + # Edit scene file path + current_directory = os.path.dirname(os.path.abspath(__file__)) + json_filepath = os.path.join(current_directory, 'utils/scene.json') + + # Load scene + ar_scene = ArScene.ArScene.from_json(json_filepath) + + # Check scene meta data + self.assertEqual(ar_scene.name, "TestScene") + self.assertEqual(ar_scene.aruco_dictionary.name, "DICT_ARUCO_ORIGINAL") + self.assertEqual(ar_scene.aruco_marker_size, 3.0) + + # Check ArUco camera + self.assertEqual(ar_scene.aruco_camera.rms, 1.0) + self.assertIsNone(numpy.testing.assert_array_equal(ar_scene.aruco_camera.dimensions, [1920, 1080])) + self.assertIsNone(numpy.testing.assert_array_equal(ar_scene.aruco_camera.K, [[1.0, 0.0, 1.0], [0.0, 1.0, 1.0], [0.0, 0.0, 1.0]])) + self.assertIsNone(numpy.testing.assert_array_equal(ar_scene.aruco_camera.D, [-1.0, -0.5, 0.0, 0.5, 1.0])) + + # Check ArUco tracker + self.assertEqual(ar_scene.aruco_tracker.tracking_data.cornerRefinementMethod, 3) + self.assertEqual(ar_scene.aruco_tracker.tracking_data.aprilTagQuadSigma, 2) + self.assertEqual(ar_scene.aruco_tracker.tracking_data.aprilTagDeglitch, 1) + + # Check ArUco scene + self.assertEqual(ar_scene.aruco_scene.angle_tolerance, 1.0) + self.assertEqual(ar_scene.aruco_scene.distance_tolerance, 2.0) + self.assertEqual(len(ar_scene.aruco_scene.places), 2) + + # Check ArUco scene places + self.assertIsNone(numpy.testing.assert_array_equal(ar_scene.aruco_scene.places['A'].translation, [1, 0, 0])) + self.assertIsNone(numpy.testing.assert_array_equal(ar_scene.aruco_scene.places['A'].rotation, [0, 0, 0])) + self.assertEqual(ar_scene.aruco_scene.places['A'].marker.identifier, 0) + + self.assertIsNone(numpy.testing.assert_array_equal(ar_scene.aruco_scene.places['B'].translation, [0, 1, 0])) + self.assertIsNone(numpy.testing.assert_array_equal(ar_scene.aruco_scene.places['B'].rotation, [0, 90, 0])) + self.assertEqual(ar_scene.aruco_scene.places['B'].marker.identifier, 1) + + # Check AOI scene + self.assertEqual(len(ar_scene.aoi_scene.items()), 1) + self.assertEqual(ar_scene.aoi_scene['Test'].size, 4) + +if __name__ == '__main__': + + unittest.main() \ No newline at end of file diff --git a/src/argaze/ArGazeScene.py b/src/argaze/ArGazeScene.py deleted file mode 100644 index 0eb4ebc..0000000 --- a/src/argaze/ArGazeScene.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python - -from typing import TypeVar -from dataclasses import dataclass, field -import json -import os - -from argaze.ArUcoMarkers import * -from argaze.AreaOfInterest import * - -import numpy - -ArGazeSceneType = TypeVar('ArGazeScene', bound="ArGazeScene") -# Type definition for type annotation convenience - -@dataclass -class ArGazeScene(): - """Define an Augmented Reality environnement thanks to ArUco markers and project it onto incoming frames.""" - - name: str - """Project name.""" - - aruco_dictionary: ArUcoMarkersDictionary.ArUcoMarkersDictionary = field(init=False, default_factory=ArUcoMarkersDictionary.ArUcoMarkersDictionary) - """ArUco markers dictionary.""" - - aruco_marker_size: float = field(init=False) - """Size of ArUco markers in centimeter.""" - - aruco_camera: ArUcoCamera.ArUcoCamera = field(init=False, default_factory=ArUcoCamera.ArUcoCamera) - """ArUco camera ...""" - - aruco_tracker: ArUcoTracker.ArUcoTracker = field(init=False, default_factory=ArUcoTracker.ArUcoTracker) - """ArUco tracker ...""" - - aruco_scene: ArUcoScene.ArUcoScene = field(init=False, default_factory=ArUcoScene.ArUcoScene) - """ArUco scene ...""" - - aoi_scene: AOI3DScene.AOI3DScene = field(init=False, default_factory=AOI3DScene.AOI3DScene) - """AOI 3D scene ...""" - - def __init__(self, **kwargs): - - self.aruco_dictionary = ArUcoMarkersDictionary.ArUcoMarkersDictionary(kwargs.pop('aruco_dictionary')) - - self.aruco_marker_size = kwargs.pop('aruco_marker_size') - - self.aruco_camera = ArUcoCamera.ArUcoCamera(**kwargs.pop('aruco_camera')) - - self.aruco_tracker = ArUcoTracker.ArUcoTracker(self.aruco_dictionary, self.aruco_marker_size, self.aruco_camera, **kwargs.pop('aruco_tracker')) - - # Check aruco_scene.places value type - aruco_scene_places_value = kwargs['aruco_scene']['places'] - - # str: relative path to .obj file - if type(aruco_scene_places_value) == str: - - kwargs['aruco_scene']['places'] = os.path.join(self.__current_directory, aruco_scene_places_value) - - self.aruco_scene = ArUcoScene.ArUcoScene(self.aruco_dictionary, self.aruco_marker_size, **kwargs.pop('aruco_scene')) - - # Check aoi_scene value type - aoi_scene_value = kwargs.pop('aoi_scene') - - # str: relative path to .obj file - if type(aoi_scene_value) == str: - - obj_filepath = os.path.join(self.__current_directory, aoi_scene_value) - self.aoi_scene = AOI3DScene.AOI3DScene.from_obj(obj_filepath) - - # dict: all AOI - else: - self.aoi_scene = AOI3DScene.AOI3DScene(aoi_scene_value) - - self.__dict__.update(kwargs) - - @classmethod - def from_json(self, json_filepath: str) -> ArGazeSceneType: - """Load ArGaze project from .json file.""" - - with open(json_filepath) as configuration_file: - - # Store current directory to allow relative path loading - self.__current_directory = os.path.dirname(os.path.abspath(json_filepath)) - - return ArGazeScene(**json.load(configuration_file)) - - def __str__(self) -> str: - """String display""" - - output = '' - output += f'\nArUcoCamera: {self.aruco_camera}' - output += f'\n\nArUcoTracker tracking data: {self.aruco_tracker.tracking_data}' - output += f'\n\nArUcoScene: {self.aruco_scene}' - output += f'\n\nAOIScene: {self.aoi_scene}' - - return output - - def project(self, frame, valid_markers:int = 1, visual_hfov=0): - """Project ArGazeScene into frame.""" - - # Track markers with pose estimation and draw them - self.aruco_tracker.track(frame) - - # When no marker is detected, no AOI scene projection can't be done - if len(self.aruco_tracker.tracked_markers) == 0: - - raise UserWarning('No marker detected') - - # Estimate set pose from tracked markers - tvec, rvec, success, validity, unvalid = self.aruco_scene.estimate_pose(self.aruco_tracker.tracked_markers) - - # When pose estimation fails, ignore AOI scene projection - if not success: - - raise UserWarning('Pose estimation fails') - - # Consider pose estimation only if it is validated by a given number of valid markers at least - elif validity >= valid_markers: - - # Clip AOI out of the horizontal visual field of view (optional) - if visual_hfov > 0: - - # Transform scene into camera referential - aoi_scene_camera_ref = self.aoi_scene.transform(tvec, rvec) - - # Get aoi inside vision cone field - cone_vision_height_cm = 200 # cm - cone_vision_radius_cm = numpy.tan(numpy.deg2rad(visual_hfov / 2)) * cone_vision_height_cm - - _, aoi_outside = aoi_scene_camera_ref.vision_cone(cone_vision_radius_cm, cone_vision_height_cm) - - # Keep only aoi inside vision cone field - aoi_scene_copy = self.aoi_scene.copy(exclude=aoi_outside.keys()) - - else: - - aoi_scene_copy = self.aoi_scene.copy() - - # DON'T APPLY CAMERA DISTORSION : it projects points which are far from the frame into it - # This hack isn't realistic but as the gaze will mainly focus on centered AOI, where the distorsion is low, it is acceptable. - aoi_scene_projection = aoi_scene_copy.project(tvec, rvec, self.aruco_camera.K) - - # Warn user when the merged scene is empty - if len(aoi_scene_projection.keys()) == 0: - - raise UserWarning('AOI projection is empty') - - return aoi_scene_projection, unvalid diff --git a/src/argaze/ArScene.py b/src/argaze/ArScene.py new file mode 100644 index 0000000..0a88b0c --- /dev/null +++ b/src/argaze/ArScene.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python + +from typing import TypeVar +from dataclasses import dataclass, field +import json +import os + +from argaze.ArUcoMarkers import * +from argaze.AreaOfInterest import * + +import numpy + +ArSceneType = TypeVar('ArScene', bound="ArScene") +# Type definition for type annotation convenience + +@dataclass +class ArScene(): + """Define an Augmented Reality environnement thanks to ArUco markers and project it onto incoming frames.""" + + name: str + """Project name.""" + + aruco_dictionary: ArUcoMarkersDictionary.ArUcoMarkersDictionary = field(init=False, default_factory=ArUcoMarkersDictionary.ArUcoMarkersDictionary) + """ArUco markers dictionary.""" + + aruco_marker_size: float = field(init=False) + """Size of ArUco markers in centimeter.""" + + aruco_camera: ArUcoCamera.ArUcoCamera = field(init=False, default_factory=ArUcoCamera.ArUcoCamera) + """ArUco camera ...""" + + aruco_tracker: ArUcoTracker.ArUcoTracker = field(init=False, default_factory=ArUcoTracker.ArUcoTracker) + """ArUco tracker ...""" + + aruco_scene: ArUcoScene.ArUcoScene = field(init=False, default_factory=ArUcoScene.ArUcoScene) + """ArUco scene ...""" + + aoi_scene: AOI3DScene.AOI3DScene = field(init=False, default_factory=AOI3DScene.AOI3DScene) + """AOI 3D scene ...""" + + def __init__(self, **kwargs): + + self.aruco_dictionary = ArUcoMarkersDictionary.ArUcoMarkersDictionary(kwargs.pop('aruco_dictionary')) + + self.aruco_marker_size = kwargs.pop('aruco_marker_size') + + self.aruco_camera = ArUcoCamera.ArUcoCamera(**kwargs.pop('aruco_camera')) + + self.aruco_tracker = ArUcoTracker.ArUcoTracker(self.aruco_dictionary, self.aruco_marker_size, self.aruco_camera, **kwargs.pop('aruco_tracker')) + + # Check aruco_scene.places value type + aruco_scene_places_value = kwargs['aruco_scene']['places'] + + # str: relative path to .obj file + if type(aruco_scene_places_value) == str: + + kwargs['aruco_scene']['places'] = os.path.join(self.__current_directory, aruco_scene_places_value) + + self.aruco_scene = ArUcoScene.ArUcoScene(self.aruco_dictionary, self.aruco_marker_size, **kwargs.pop('aruco_scene')) + + # Check aoi_scene value type + aoi_scene_value = kwargs.pop('aoi_scene') + + # str: relative path to .obj file + if type(aoi_scene_value) == str: + + obj_filepath = os.path.join(self.__current_directory, aoi_scene_value) + self.aoi_scene = AOI3DScene.AOI3DScene.from_obj(obj_filepath) + + # dict: all AOI + else: + self.aoi_scene = AOI3DScene.AOI3DScene(aoi_scene_value) + + self.__dict__.update(kwargs) + + @classmethod + def from_json(self, json_filepath: str) -> ArSceneType: + """Load ArGaze project from .json file.""" + + with open(json_filepath) as configuration_file: + + # Store current directory to allow relative path loading + self.__current_directory = os.path.dirname(os.path.abspath(json_filepath)) + + return ArScene(**json.load(configuration_file)) + + def __str__(self) -> str: + """String display""" + + output = '' + output += f'\nArUcoCamera: {self.aruco_camera}' + output += f'\n\nArUcoTracker tracking data: {self.aruco_tracker.tracking_data}' + output += f'\n\nArUcoScene: {self.aruco_scene}' + output += f'\n\nAOIScene: {self.aoi_scene}' + + return output + + def project(self, frame, consistent_markers_number:int = 1, visual_hfov=0): + """Project ArScene into frame.""" + + # Track markers with pose estimation and draw them + self.aruco_tracker.track(frame) + + # When no marker is detected, no AOI scene projection can't be done + if len(self.aruco_tracker.tracked_markers) == 0: + + raise UserWarning('No marker detected') + + # Estimate set pose from tracked markers + tvec, rvec, success, consistent_markers, unconsistencies = self.aruco_scene.estimate_pose(self.aruco_tracker.tracked_markers) + + # When pose estimation fails, ignore AOI scene projection + if not success: + + raise UserWarning('Pose estimation fails') + + # Consider pose estimation only if theer is a given number of consistent markers at least + elif len(consistent_markers) >= consistent_markers_number: + + # Clip AOI out of the visual horizontal field of view (optional) + if visual_hfov > 0: + + # Transform scene into camera referential + aoi_scene_camera_ref = self.aoi_scene.transform(tvec, rvec) + + # Get aoi inside vision cone field + cone_vision_height_cm = 200 # cm + cone_vision_radius_cm = numpy.tan(numpy.deg2rad(visual_hfov / 2)) * cone_vision_height_cm + + _, aoi_outside = aoi_scene_camera_ref.vision_cone(cone_vision_radius_cm, cone_vision_height_cm) + + # Keep only aoi inside vision cone field + aoi_scene_copy = self.aoi_scene.copy(exclude=aoi_outside.keys()) + + else: + + aoi_scene_copy = self.aoi_scene.copy() + + # DON'T APPLY CAMERA DISTORSION : it projects points which are far from the frame into it + # This hack isn't realistic but as the gaze will mainly focus on centered AOI, where the distorsion is low, it is acceptable. + aoi_scene_projection = aoi_scene_copy.project(tvec, rvec, self.aruco_camera.K) + + # Warn user when the merged scene is empty + if len(aoi_scene_projection.keys()) == 0: + + raise UserWarning('AOI projection is empty') + + return aoi_scene_projection, unconsistencies diff --git a/src/argaze/ArUcoMarkers/ArUcoScene.py b/src/argaze/ArUcoMarkers/ArUcoScene.py index 857ebf4..b267102 100644 --- a/src/argaze/ArUcoMarkers/ArUcoScene.py +++ b/src/argaze/ArUcoMarkers/ArUcoScene.py @@ -77,7 +77,7 @@ class ArUcoScene(): self._translation = numpy.zeros(3) self._rotation = numpy.zeros(3) self._succeded = False - self._validity = 0 + self._consistent_markers = 0 # Process markers ids to speed up further calculations self.__identifier_cache = {} @@ -308,7 +308,7 @@ class ArUcoScene(): I = numpy.identity(3, dtype = R.dtype) return numpy.linalg.norm(I - numpy.dot(R.T, R)) < 1e-6 - def __normalise_place_pose(self, name, place, F): + def __normalise_marker_pose(self, name, place, F): # Transform place rotation into scene rotation vector R = self.__rotation_cache[name] @@ -328,73 +328,73 @@ class ArUcoScene(): return rvec, tvec def estimate_pose(self, tracked_markers) -> Tuple[numpy.array, numpy.array, bool, int, dict]: - """Estimate scene pose from tracked markers (cf ArUcoTracker.track()) + """Estimate scene pose from tracked markers (cf ArUcoTracker.track()) and validate its consistency according expected scene places. * **Returns:** - - translation vector - - rotation vector - - pose estimation success status - - the number of places used to estimate the pose as validity score - - dict of non valid distance and angle + - scene translation vector + - scene rotation vector + - scene pose estimation success status + - all tracked markers considered as consistent and used to estimate the pose + - dict of identified distance or angle unconsistencies and out-of-bounds values """ # Init pose data self._translation = numpy.zeros(3) self._rotation = numpy.zeros(3) self._succeded = False - self._validity = 0 - self._unvalid = {} + self._consistent_markers = {} + self._unconsistencies = {} # Don't try to estimate pose if there is no tracked markers if len(tracked_markers) == 0: - return self._translation, self._rotation, self._succeded, self._validity, self._unvalid + return self._translation, self._rotation, self._succeded, self._consistent_markers, self._unconsistencies - # Look for places related to tracked markers - tracked_places = {} + # Look for tracked markers which belong to the scene and store them by name + scene_markers = {} for (marker_id, marker) in tracked_markers.items(): try: name = self.__identifier_cache[marker_id] - tracked_places[name] = marker + scene_markers[name] = marker except KeyError: continue #print('-------------- ArUcoScene pose estimation --------------') - # Pose validity checking is'nt possible when only one place of the scene is tracked - if len(tracked_places.keys()) == 1: + # Pose consistency checking is'nt possible when only one marker is tracked + if len(scene_markers) == 1: - # Get scene pose from to the unique place pose - name, place = tracked_places.popitem() - F, _ = cv.Rodrigues(place.rotation) + # Get scene pose from to the unique marker + name, marker = scene_markers.popitem() + F, _ = cv.Rodrigues(marker.rotation) - self._rotation, self._translation = self.__normalise_place_pose(name, place, F) + self._rotation, self._translation = self.__normalise_marker_pose(name, marker, F) self._succeded = True - self._validity = 1 + self._consistent_markers[name] = marker #print('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') #print(f'ArUcoScene rotation vector: {self._rotation[0][0]:3f} {self._rotation[1][0]:3f} {self._rotation[2][0]:3f}') #print(f'ArUcoScene translation vector: {self._translation[0]:3f} {self._translation[1]:3f} {self._translation[2]:3f}') - # Pose validity checking processes places two by two + # Pose consistency checking processes markers two by two else: - valid_places = [] - valid_rvecs = [] - valid_tvecs = [] + consistent_markers = [] + consistent_rvecs = [] + consistent_tvecs = [] - for (A_name, A_place), (B_name, B_place) in itertools.combinations(tracked_places.items(), 2): + for (A_name, A_marker), (B_name, B_marker) in itertools.combinations(scene_markers.items(), 2): #print(f'** {A_name} > {B_name}') - # Get place rotation estimation + # Get marker rotation estimation # Use rotation matrix instead of rotation vector - A, _ = cv.Rodrigues(A_place.rotation) - B, _ = cv.Rodrigues(B_place.rotation) + A, _ = cv.Rodrigues(A_marker.rotation) + B, _ = cv.Rodrigues(B_marker.rotation) - # Rotation matrix from A place to B place + # Rotation matrix from A marker to B marker AB = B.dot(A.T) assert(self.__is_rotation_matrix(AB)) @@ -403,102 +403,102 @@ class ArUcoScene(): angle = numpy.rad2deg(numpy.arccos((numpy.trace(AB) - 1) / 2)) expected_angle = self.__angle_cache[A_name][B_name] - # Calculate distance between A place center and B place center - distance = numpy.linalg.norm(A_place.translation - B_place.translation) + # Calculate distance between A marker center and B marker center + distance = numpy.linalg.norm(A_marker.translation - B_marker.translation) expected_distance = self.__distance_cache[A_name][B_name] - # Check angle and distance according given tolerance then normalise place pose - valid_angle = math.isclose(angle, expected_angle, abs_tol=self.angle_tolerance) - valid_distance = math.isclose(distance, expected_distance, abs_tol=self.distance_tolerance) + # Check angle and distance according given tolerance then normalise marker pose + consistent_angle = math.isclose(angle, expected_angle, abs_tol=self.angle_tolerance) + consistent_distance = math.isclose(distance, expected_distance, abs_tol=self.distance_tolerance) - if valid_angle and valid_distance: + if consistent_angle and consistent_distance: - if A_name not in valid_places: + if A_name not in consistent_markers: - # Remember this place is already validated - valid_places.append(A_name) + # Remember this marker is already validated + consistent_markers.append(A_name) - rvec, tvec = self.__normalise_place_pose(A_name, A_place, A) + rvec, tvec = self.__normalise_marker_pose(A_name, A_marker, A) - # Store normalised place pose - valid_rvecs.append(rvec) - valid_tvecs.append(tvec) + # Store normalised marker pose + consistent_rvecs.append(rvec) + consistent_tvecs.append(tvec) - if B_name not in valid_places: + if B_name not in consistent_markers: - # Remember this place is already validated - valid_places.append(B_name) + # Remember this marker is already validated + consistent_markers.append(B_name) - rvec, tvec = self.__normalise_place_pose(B_name, B_place, B) + rvec, tvec = self.__normalise_marker_pose(B_name, B_marker, B) - # Store normalised place pose - valid_rvecs.append(rvec) - valid_tvecs.append(tvec) + # Store normalised marker pose + consistent_rvecs.append(rvec) + consistent_tvecs.append(tvec) else: - if not valid_angle: - self._unvalid[f'{A_name}/{B_name} angle'] = angle + if not consistent_angle: + self._unconsistencies[f'{A_name}/{B_name} angle'] = angle - if not valid_distance: - self._unvalid[f'{A_name}/{B_name} distance'] = distance + if not consistent_distance: + self._unconsistencies[f'{A_name}/{B_name} distance'] = distance - if len(valid_places) > 1: + if len(consistent_markers) > 1: - # Consider ArUcoScene rotation as the mean of all valid translations + # Consider ArUcoScene rotation as the mean of all consistent translations # !!! WARNING !!! This is a bad hack : processing rotations average is a very complex problem that needs to well define the distance calculation method before. - self._rotation = numpy.mean(numpy.array(valid_rvecs), axis=0) + self._rotation = numpy.mean(numpy.array(consistent_rvecs), axis=0) - # Consider ArUcoScene translation as the mean of all valid translations - self._translation = numpy.mean(numpy.array(valid_tvecs), axis=0) + # Consider ArUcoScene translation as the mean of all consistent translations + self._translation = numpy.mean(numpy.array(consistent_tvecs), axis=0) #print(':::::::::::::::::::::::::::::::::::::::::::::::::::') #print(f'ArUcoScene rotation vector: {self._rotation[0][0]:3f} {self._rotation[1][0]:3f} {self._rotation[2][0]:3f}') #print(f'ArUcoScene translation vector: {self._translation[0]:3f} {self._translation[1]:3f} {self._translation[2]:3f}') self._succeded = True - self._validity = len(valid_places) + self._consistent_markers = consistent_markers else: - unvalid_rvecs = [] - unvalid_tvecs = [] + unconsistent_rvecs = [] + unconsistent_tvecs = [] - # Gather unvalid pose estimations - for name, place in tracked_places.items(): + # Gather unconsistent pose estimations + for name, marker in scene_markers.items(): - if name not in valid_places: + if name not in consistent_markers: - R, _ = cv.Rodrigues(place.rotation) - rvec, tvec = self.__normalise_place_pose(name, place, R) + R, _ = cv.Rodrigues(marker.rotation) + rvec, tvec = self.__normalise_marker_pose(name, marker, R) - unvalid_rvecs = [rvec] - unvalid_tvecs = [tvec] + unconsistent_rvecs = [rvec] + unconsistent_tvecs = [tvec] - # Consider ArUcoScene rotation as the mean of all unvalid translations + # Consider ArUcoScene rotation as the mean of all unconsistent translations # !!! WARNING !!! This is a bad hack : processing rotations average is a very complex problem that needs to well define the distance calculation method before. - self._rotation = numpy.mean(numpy.array(unvalid_rvecs), axis=0) + self._rotation = numpy.mean(numpy.array(unconsistent_rvecs), axis=0) - # Consider ArUcoScene translation as the mean of all unvalid translations - self._translation = numpy.mean(numpy.array(unvalid_tvecs), axis=0) + # Consider ArUcoScene translation as the mean of all unconsistent translations + self._translation = numpy.mean(numpy.array(unconsistent_tvecs), axis=0) #print(':::::::::::::::::::::::::::::::::::::::::::::::::::') #print(f'ArUcoScene rotation vector: {self._rotation[0][0]:3f} {self._rotation[1][0]:3f} {self._rotation[2][0]:3f}') #print(f'ArUcoScene translation vector: {self._translation[0]:3f} {self._translation[1]:3f} {self._translation[2]:3f}') self._succeded = False - self._validity = len(tracked_places) + self._consistent_markers = {} #print('----------------------------------------------------') - return self._translation, self._rotation, self._succeded, self._validity, self._unvalid + return self._translation, self._rotation, self._succeded, self._consistent_markers, self._unconsistencies @property def translation(self) -> numpy.array: """Access to scene translation vector. .. warning:: - Setting scene translation vector implies succeded status to be True and validity score to be 0.""" + Setting scene translation vector implies succeded status to be True and consistency score to be 0.""" return self._translation @@ -507,14 +507,14 @@ class ArUcoScene(): self._translation = tvec self._succeded = True - self._validity = 0 + self._consistent_markers = 0 @property def rotation(self) -> numpy.array: """Access to scene rotation vector. .. warning:: - Setting scene rotation vector implies succeded status to be True and validity score to be 0.""" + Setting scene rotation vector implies succeded status to be True and consistency score to be 0.""" return self._translation @@ -523,7 +523,7 @@ class ArUcoScene(): self._rotation = rvec self._succeded = True - self._validity = 0 + self._consistent_markers = 0 @property def succeded(self) -> bool: @@ -532,10 +532,10 @@ class ArUcoScene(): return self._succeded @property - def validity(self) -> int: - """Access to scene pose estimation validity score.""" + def consistency(self) -> int: + """Access to scene pose estimation consistency score.""" - return self._validity + return len(self._consistent_markers) def draw(self, frame, K, D, draw_places=True): """Draw scene axis and places.""" @@ -543,9 +543,9 @@ class ArUcoScene(): l = self.__marker_size / 2 ll = self.__marker_size - # Select color according validity score - n = 95 * self._validity if self._validity < 2 else 0 - f = 159 * self._validity if self._validity < 2 else 255 + # Select color according consistency score + n = 95 * self.consistency if self.consistency < 2 else 0 + f = 159 * self.consistency if self.consistency < 2 else 255 try: @@ -584,5 +584,5 @@ class ArUcoScene(): print(self._translation) print(self._rotation) print(self._succeded) - print(self._validity) + print(self._consistent_markers) print(axisPoints) diff --git a/src/argaze/__init__.py b/src/argaze/__init__.py index 1e1b5bb..f02a31a 100644 --- a/src/argaze/__init__.py +++ b/src/argaze/__init__.py @@ -2,4 +2,4 @@ .. include:: ../../README.md """ __docformat__ = "restructuredtext" -__all__ = ['utils','ArUcoMarkers','AreaOfInterest','GazeFeatures','DataStructures','GazeAnalysis','ArGazeScene','TobiiGlassesPro2'] \ No newline at end of file +__all__ = ['utils','ArUcoMarkers','AreaOfInterest','GazeFeatures','DataStructures','GazeAnalysis','ArScene','TobiiGlassesPro2'] \ No newline at end of file diff --git a/src/argaze/utils/tobii_segment_argaze_scene_export.py b/src/argaze/utils/tobii_segment_argaze_scene_export.py deleted file mode 100644 index a038e34..0000000 --- a/src/argaze/utils/tobii_segment_argaze_scene_export.py +++ /dev/null @@ -1,235 +0,0 @@ -#!/usr/bin/env python - -import argparse -import os, json -import math -import threading - -from argaze import * -from argaze.TobiiGlassesPro2 import * -from argaze.ArUcoMarkers import * -from argaze.AreaOfInterest import * -from argaze.utils import MiscFeatures - -import cv2 as cv -import numpy - -def make_rotation_matrix(x, y, z): - - # Create rotation matrix around x axis - c = numpy.cos(numpy.deg2rad(x)) - s = numpy.sin(numpy.deg2rad(x)) - Rx = numpy.array([[1, 0, 0], [0, c, -s], [0, s, c]]) - - # Create rotation matrix around y axis - c = numpy.cos(numpy.deg2rad(y)) - s = numpy.sin(numpy.deg2rad(y)) - Ry = numpy.array([[c, 0, s], [0, 1, 0], [-s, 0, c]]) - - # Create rotation matrix around z axis - c = numpy.cos(numpy.deg2rad(z)) - s = numpy.sin(numpy.deg2rad(z)) - Rz = numpy.array([[c, -s, 0], [s, c, 0], [0, 0, 1]]) - - # Return intrinsic rotation matrix - return Rx.dot(Ry.dot(Rz)) - -def main(): - """ - Track ArUcoPlan into Tobii Glasses Pro 2 camera video stream. - """ - - # Manage arguments - parser = argparse.ArgumentParser(description=main.__doc__.split('-')[0]) - parser.add_argument('-s', '--segment_path', metavar='SEGMENT_PATH', type=str, default=None, help='segment path') - parser.add_argument('-t', '--time_range', metavar=('START_TIME', 'END_TIME'), nargs=2, type=float, default=(0., None), help='start and end time (in second)') - parser.add_argument('-p', '--project_path', metavar='ARGAZE_PROJECT', type=str, default=None, help='json argaze project filepath') - parser.add_argument('-o', '--output', metavar='OUT', type=str, default=None, help='destination folder path (segment folder by default)') - parser.add_argument('-w', '--window', metavar='DISPLAY', type=bool, default=True, help='enable window display', action=argparse.BooleanOptionalAction) - args = parser.parse_args() - - if args.segment_path != None: - - # Manage destination path - destination_path = '.' - if args.output != None: - - if not os.path.exists(os.path.dirname(args.output)): - - os.makedirs(os.path.dirname(args.output)) - print(f'{os.path.dirname(args.output)} folder created') - - destination_path = args.output - - else: - - destination_path = args.segment_path - - # Export into a dedicated time range folder - if args.time_range[1] != None: - timerange_path = f'[{int(args.time_range[0])}s - {int(args.time_range[1])}s]' - else: - timerange_path = f'[all]' - - destination_path = f'{destination_path}/{timerange_path}' - - if not os.path.exists(destination_path): - - os.makedirs(destination_path) - print(f'{destination_path} folder created') - - vs_data_filepath = f'{destination_path}/aoi.csv' - vs_video_filepath = f'{destination_path}/aoi.mp4' - - # Load a tobii segment - tobii_segment = TobiiEntities.TobiiSegment(args.segment_path, int(args.time_range[0] * 1e6), int(args.time_range[1] * 1e6) if args.time_range[1] != None else None) - - # Load a tobii segment video - tobii_segment_video = tobii_segment.load_video() - print(f'\nVideo properties:\n\tduration: {tobii_segment_video.duration/1e6} s\n\twidth: {tobii_segment_video.width} px\n\theight: {tobii_segment_video.height} px') - - # Load a tobii segment data - tobii_segment_data = tobii_segment.load_data() - - print(f'\nLoaded data count:') - for name in tobii_segment_data.keys(): - print(f'\t{name}: {len(tobii_segment_data[name])} data') - - # Access to video timestamp data buffer - tobii_ts_vts = tobii_segment_data['VideoTimeStamp'] - - # Prepare video exportation at the same format than segment video - output_video = TobiiVideo.TobiiVideoOutput(vs_video_filepath, tobii_segment_video.stream) - - # Load argaze project - argaze_scene = ArGazeScene.ArGazeScene.from_json(args.project_path) - - print(argaze_scene) - - # Create timestamped buffer to store AOIs and primary time stamp offset - ts_offset_aois = DataStructures.TimeStampedBuffer() - - # Video and data replay loop - try: - - # Initialise progress bar - #MiscFeatures.printProgressBar(0, tobii_segment_video.duration/1e3, prefix = 'Progress:', suffix = 'Complete', length = 100) - - # Iterate on video frames - for video_ts, video_frame in tobii_segment_video.frames(): - - video_ts_ms = video_ts / 1e3 - - # Copy video frame to edit visualisation on it without disrupting aruco tracking - visu_frame = video_frame.copy() - - # Prepare to store projected AOI - projected_aois = {} - - # Process video and data frame - try: - - # Get nearest video timestamp - _, nearest_vts = tobii_ts_vts.get_last_before(video_ts) - - projected_aois['offset'] = nearest_vts - - # Hide frame left and right borders before tracking to ignore markers outside focus area - cv.rectangle(video_frame.matrix, (0, 0), (int(video_frame.width/6), int(video_frame.height)), (0, 0, 0), -1) - cv.rectangle(video_frame.matrix, (int(video_frame.width*(1 - 1/6)), 0), (int(video_frame.width), int(video_frame.height)), (0, 0, 0), -1) - - # Project scene into frame - scene_projection, unvalid = argaze_scene.project(video_frame.matrix, valid_markers=1, visual_hfov=TobiiSpecifications.VISUAL_HFOV) - - # DEBUG: print unvalid distances or angles - for key, value in unvalid.items(): - print(f'{video_ts}: Unvalid {key}: {value}.') - - # Store all projected aoi - for aoi_name in scene_projection.keys(): - - projected_aois[aoi_name] = numpy.rint(scene_projection[aoi_name]).astype(int) - - # Draw tracked markers - argaze_scene.aruco_tracker.draw_tracked_markers(visu_frame.matrix) - - # Draw scene projection - scene_projection.draw(visu_frame.matrix, (0, 0), color=(0, 255, 255)) - - # Catch warnings raised by project_scene method - except UserWarning as w: - - projected_aois['warning'] = w - - # Draw tracked markers - argaze_scene.aruco_tracker.draw_tracked_markers(visu_frame.matrix) - - if w == 'Pose estimation fails': - - # Draw black AOI scene - scene_projection.draw(visu_frame.matrix, (0, 0), color=(0, 0, 0)) - - cv.rectangle(visu_frame.matrix, (0, 50), (550, 100), (127, 127, 127), -1) - cv.putText(visu_frame.matrix, str(w), (20, 80), cv.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 1, cv.LINE_AA) - - # Raised when timestamped buffer is empty - except KeyError as e: - pass - - # Store projected AOI - ts_offset_aois[video_ts] = projected_aois - - # Draw focus area - cv.rectangle(visu_frame.matrix, (int(video_frame.width/6), 0), (int(visu_frame.width*(1-1/6)), int(visu_frame.height)), (255, 150, 150), 1) - - # Draw center - cv.line(visu_frame.matrix, (int(visu_frame.width/2) - 50, int(visu_frame.height/2)), (int(visu_frame.width/2) + 50, int(visu_frame.height/2)), (255, 150, 150), 1) - cv.line(visu_frame.matrix, (int(visu_frame.width/2), int(visu_frame.height/2) - 50), (int(visu_frame.width/2), int(visu_frame.height/2) + 50), (255, 150, 150), 1) - - # Write segment timing - cv.rectangle(visu_frame.matrix, (0, 0), (550, 50), (63, 63, 63), -1) - cv.putText(visu_frame.matrix, f'Segment time: {int(video_ts_ms)} ms', (20, 40), cv.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 1, cv.LINE_AA) - - if args.window: - - # Close window using 'Esc' key - if cv.waitKey(1) == 27: - break - - # Display visualisation - cv.imshow(f'Segment {tobii_segment.id} ArUco AOI', visu_frame.matrix) - - # Write video - output_video.write(visu_frame.matrix) - - # Update Progress Bar - progress = video_ts_ms - int(args.time_range[0] * 1e3) - #MiscFeatures.printProgressBar(progress, tobii_segment_video.duration/1e3, prefix = 'Progress:', suffix = 'Complete', length = 100) - - # Exit on 'ctrl+C' interruption - except KeyboardInterrupt: - pass - - # Stop frame display - cv.destroyAllWindows() - - # End output video file - output_video.close() - - # Print aruco tracking metrics - print('\nAruco marker tracking metrics') - try_count, tracked_counts = argaze_scene.aruco_tracker.track_metrics - - for marker_id, tracked_count in tracked_counts.items(): - print(f'Markers {marker_id} has been detected in {tracked_count} / {try_count} frames ({round(100 * tracked_count / try_count, 2)} %)') - - # Export aruco aoi data - ts_offset_aois.as_dataframe().to_csv(vs_data_filepath, index=True) - print(f'Aruco AOI data saved into {vs_data_filepath}') - - # Notify when the aruco aoi video has been exported - print(f'Aruco AOI video saved into {vs_video_filepath}') - -if __name__ == '__main__': - - main() \ No newline at end of file diff --git a/src/argaze/utils/tobii_segment_arscene_export.py b/src/argaze/utils/tobii_segment_arscene_export.py new file mode 100644 index 0000000..7bbef59 --- /dev/null +++ b/src/argaze/utils/tobii_segment_arscene_export.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python + +import argparse +import os, json +import math +import threading + +from argaze import * +from argaze.TobiiGlassesPro2 import * +from argaze.ArUcoMarkers import * +from argaze.AreaOfInterest import * +from argaze.utils import MiscFeatures + +import cv2 as cv +import numpy + +def make_rotation_matrix(x, y, z): + + # Create rotation matrix around x axis + c = numpy.cos(numpy.deg2rad(x)) + s = numpy.sin(numpy.deg2rad(x)) + Rx = numpy.array([[1, 0, 0], [0, c, -s], [0, s, c]]) + + # Create rotation matrix around y axis + c = numpy.cos(numpy.deg2rad(y)) + s = numpy.sin(numpy.deg2rad(y)) + Ry = numpy.array([[c, 0, s], [0, 1, 0], [-s, 0, c]]) + + # Create rotation matrix around z axis + c = numpy.cos(numpy.deg2rad(z)) + s = numpy.sin(numpy.deg2rad(z)) + Rz = numpy.array([[c, -s, 0], [s, c, 0], [0, 0, 1]]) + + # Return intrinsic rotation matrix + return Rx.dot(Ry.dot(Rz)) + +def main(): + """ + Track ArUcoPlan into Tobii Glasses Pro 2 camera video stream. + """ + + # Manage arguments + parser = argparse.ArgumentParser(description=main.__doc__.split('-')[0]) + parser.add_argument('-s', '--segment_path', metavar='SEGMENT_PATH', type=str, default=None, help='segment path') + parser.add_argument('-t', '--time_range', metavar=('START_TIME', 'END_TIME'), nargs=2, type=float, default=(0., None), help='start and end time (in second)') + parser.add_argument('-p', '--project_path', metavar='ARGAZE_PROJECT', type=str, default=None, help='json argaze project filepath') + parser.add_argument('-o', '--output', metavar='OUT', type=str, default=None, help='destination folder path (segment folder by default)') + parser.add_argument('-w', '--window', metavar='DISPLAY', type=bool, default=True, help='enable window display', action=argparse.BooleanOptionalAction) + args = parser.parse_args() + + if args.segment_path != None: + + # Manage destination path + destination_path = '.' + if args.output != None: + + if not os.path.exists(os.path.dirname(args.output)): + + os.makedirs(os.path.dirname(args.output)) + print(f'{os.path.dirname(args.output)} folder created') + + destination_path = args.output + + else: + + destination_path = args.segment_path + + # Export into a dedicated time range folder + if args.time_range[1] != None: + timerange_path = f'[{int(args.time_range[0])}s - {int(args.time_range[1])}s]' + else: + timerange_path = f'[all]' + + destination_path = f'{destination_path}/{timerange_path}' + + if not os.path.exists(destination_path): + + os.makedirs(destination_path) + print(f'{destination_path} folder created') + + vs_data_filepath = f'{destination_path}/aoi.csv' + vs_video_filepath = f'{destination_path}/aoi.mp4' + + # Load a tobii segment + tobii_segment = TobiiEntities.TobiiSegment(args.segment_path, int(args.time_range[0] * 1e6), int(args.time_range[1] * 1e6) if args.time_range[1] != None else None) + + # Load a tobii segment video + tobii_segment_video = tobii_segment.load_video() + print(f'\nVideo properties:\n\tduration: {tobii_segment_video.duration/1e6} s\n\twidth: {tobii_segment_video.width} px\n\theight: {tobii_segment_video.height} px') + + # Load a tobii segment data + tobii_segment_data = tobii_segment.load_data() + + print(f'\nLoaded data count:') + for name in tobii_segment_data.keys(): + print(f'\t{name}: {len(tobii_segment_data[name])} data') + + # Access to video timestamp data buffer + tobii_ts_vts = tobii_segment_data['VideoTimeStamp'] + + # Access to timestamped gaze position data buffer + tobii_ts_gaze_positions = tobii_segment_data['GazePosition'] + + # Prepare video exportation at the same format than segment video + output_video = TobiiVideo.TobiiVideoOutput(vs_video_filepath, tobii_segment_video.stream) + + # Load ar scene + ar_scene = ArScene.ArScene.from_json(args.project_path) + + print(ar_scene) + + # Create timestamped buffer to store AOIs and primary time stamp offset + ts_offset_aois = DataStructures.TimeStampedBuffer() + + # Video and data replay loop + try: + + # Initialise progress bar + #MiscFeatures.printProgressBar(0, tobii_segment_video.duration/1e3, prefix = 'Progress:', suffix = 'Complete', length = 100) + + # Iterate on video frames + for video_ts, video_frame in tobii_segment_video.frames(): + + video_ts_ms = video_ts / 1e3 + + # Copy video frame to edit visualisation on it without disrupting aruco tracking + visu_frame = video_frame.copy() + + # Prepare to store projected AOI + projected_aois = {} + + # Process video and data frame + try: + + # Get nearest video timestamp + _, nearest_vts = tobii_ts_vts.get_last_before(video_ts) + + projected_aois['offset'] = nearest_vts.offset + + # Hide frame left and right borders before tracking to ignore markers outside focus area + cv.rectangle(video_frame.matrix, (0, 0), (int(video_frame.width/6), int(video_frame.height)), (0, 0, 0), -1) + cv.rectangle(video_frame.matrix, (int(video_frame.width*(1 - 1/6)), 0), (int(video_frame.width), int(video_frame.height)), (0, 0, 0), -1) + + # Project scene into frame + scene_projection, unconsistencies = ar_scene.project(video_frame.matrix, consistent_markers_number=1, visual_hfov=TobiiSpecifications.VISUAL_HFOV) + + # DEBUG: print unconsistencies distances or angles + for key, value in unconsistencies.items(): + print(f'{video_ts}: Unconsistent {key}: {value}') + + # Store all projected aoi + for aoi_name in scene_projection.keys(): + + projected_aois[aoi_name] = numpy.rint(scene_projection[aoi_name]).astype(int) + + # Draw tracked markers + ar_scene.aruco_tracker.draw_tracked_markers(visu_frame.matrix) + + # Draw scene projection + scene_projection.draw(visu_frame.matrix, (0, 0), color=(0, 255, 255)) + + # Catch warnings raised by project_scene method + except UserWarning as w: + + projected_aois['comment'] = w + + # Draw tracked markers + ar_scene.aruco_tracker.draw_tracked_markers(visu_frame.matrix) + + if w == 'Pose estimation fails': + + # Draw black AOI scene + scene_projection.draw(visu_frame.matrix, (0, 0), color=(0, 0, 0)) + + cv.rectangle(visu_frame.matrix, (0, 50), (550, 100), (127, 127, 127), -1) + cv.putText(visu_frame.matrix, str(w), (20, 80), cv.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 1, cv.LINE_AA) + + # Raised when timestamped buffer is empty + except KeyError as e: + + e = 'VideoTimeStamp missing' + + projected_aois['offset'] = 0 + projected_aois['comment'] = e + + cv.rectangle(visu_frame.matrix, (0, 50), (550, 100), (127, 127, 127), -1) + cv.putText(visu_frame.matrix, str(e), (20, 80), cv.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 1, cv.LINE_AA) + + # Store projected AOI + ts_offset_aois[video_ts] = projected_aois + + # Draw focus area + cv.rectangle(visu_frame.matrix, (int(video_frame.width/6), 0), (int(visu_frame.width*(1-1/6)), int(visu_frame.height)), (255, 150, 150), 1) + + # Draw center + cv.line(visu_frame.matrix, (int(visu_frame.width/2) - 50, int(visu_frame.height/2)), (int(visu_frame.width/2) + 50, int(visu_frame.height/2)), (255, 150, 150), 1) + cv.line(visu_frame.matrix, (int(visu_frame.width/2), int(visu_frame.height/2) - 50), (int(visu_frame.width/2), int(visu_frame.height/2) + 50), (255, 150, 150), 1) + + # Write segment timing + cv.rectangle(visu_frame.matrix, (0, 0), (550, 50), (63, 63, 63), -1) + cv.putText(visu_frame.matrix, f'Segment time: {int(video_ts_ms)} ms', (20, 40), cv.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 1, cv.LINE_AA) + + if args.window: + + # Close window using 'Esc' key + if cv.waitKey(1) == 27: + break + + # Display visualisation + cv.imshow(f'Segment {tobii_segment.id} ArUco AOI', visu_frame.matrix) + + # Write video + output_video.write(visu_frame.matrix) + + # Update Progress Bar + progress = video_ts_ms - int(args.time_range[0] * 1e3) + #MiscFeatures.printProgressBar(progress, tobii_segment_video.duration/1e3, prefix = 'Progress:', suffix = 'Complete', length = 100) + + # Exit on 'ctrl+C' interruption + except KeyboardInterrupt: + pass + + # Stop frame display + cv.destroyAllWindows() + + # End output video file + output_video.close() + + # Print aruco tracking metrics + print('\nAruco marker tracking metrics') + try_count, tracked_counts = ar_scene.aruco_tracker.track_metrics + + for marker_id, tracked_count in tracked_counts.items(): + print(f'Markers {marker_id} has been detected in {tracked_count} / {try_count} frames ({round(100 * tracked_count / try_count, 2)} %)') + + # Export aruco aoi data + ts_offset_aois.as_dataframe().to_csv(vs_data_filepath, index=True) + print(f'Aruco AOI data saved into {vs_data_filepath}') + + # Notify when the aruco aoi video has been exported + print(f'Aruco AOI video saved into {vs_video_filepath}') + +if __name__ == '__main__': + + main() \ No newline at end of file -- cgit v1.1