From 54229c63622782d15902d2389350317f2271a8c8 Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Tue, 2 Jul 2024 12:56:58 +0200 Subject: Fixing bad admonition formatting. --- .../VelocityThresholdIdentification.py | 514 ++++++++++----------- src/argaze/ArFeatures.py | 16 +- src/argaze/ArUcoMarker/ArUcoDetector.py | 12 +- src/argaze/ArUcoMarker/ArUcoMarker.py | 102 ++-- src/argaze/ArUcoMarker/ArUcoMarkerGroup.py | 5 +- src/argaze/AreaOfInterest/AOI2DScene.py | 4 +- src/argaze/AreaOfInterest/AOI3DScene.py | 10 +- src/argaze/AreaOfInterest/AOIFeatures.py | 20 +- src/argaze/DataFeatures.py | 11 +- src/argaze/GazeAnalysis/DeviationCircleCoverage.py | 258 +++++------ src/argaze/GazeAnalysis/Entropy.py | 100 ++-- src/argaze/GazeFeatures.py | 38 +- src/argaze/__init__.py | 2 +- src/argaze/utils/UtilsFeatures.py | 4 +- src/argaze/utils/contexts/TobiiProGlasses2.py | 4 +- 15 files changed, 548 insertions(+), 552 deletions(-) (limited to 'src') diff --git a/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py b/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py index e7431b5..9bb07cb 100644 --- a/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py +++ b/src/argaze.test/GazeAnalysis/VelocityThresholdIdentification.py @@ -28,301 +28,301 @@ from argaze.GazeAnalysis import VelocityThresholdIdentification import numpy def build_gaze_fixation(size: int, center: tuple, deviation_max: float, min_time: float, max_time: float, start_ts: float = 0., validity: list = []): - """ Generate N TimeStampedGazePsoitions dispersed around a center point for testing purpose. - Timestamps are current time after random sleep (second). - GazePositions are random values. - """ - ts_gaze_positions = GazeFeatures.TimeStampedGazePositions() + """ Generate N TimeStampedGazePsoitions dispersed around a center point for testing purpose. + Timestamps are current time after random sleep (second). + GazePositions are random values. + """ + ts_gaze_positions = GazeFeatures.TimeStampedGazePositions() - start_time = time.time() + start_time = time.time() - for i in range(0, size): + for i in range(0, size): - # Sleep a random time - sleep_time = random.random() * (max_time - min_time) + min_time - time.sleep(sleep_time) + # Sleep a random time + sleep_time = random.random() * (max_time - min_time) + min_time + time.sleep(sleep_time) - # Check position validity - valid = True - if len(validity) > i: + # Check position validity + valid = True + if len(validity) > i: - valid = validity[i] + valid = validity[i] - if valid: + if valid: - # Edit gaze position - random_x = center[0] + deviation_max * (random.random() - 0.5) / math.sqrt(2) - random_y = center[1] + deviation_max * (random.random() - 0.5) / math.sqrt(2) - gaze_position = GazeFeatures.GazePosition((random_x, random_y)) + # Edit gaze position + random_x = center[0] + deviation_max * (random.random() - 0.5) / math.sqrt(2) + random_y = center[1] + deviation_max * (random.random() - 0.5) / math.sqrt(2) + gaze_position = GazeFeatures.GazePosition((random_x, random_y)) - else: + else: - gaze_position = GazeFeatures.GazePosition() + gaze_position = GazeFeatures.GazePosition() - # Timestamp gaze position - gaze_position.timestamp = time.time() - start_time + start_ts + # Timestamp gaze position + gaze_position.timestamp = time.time() - start_time + start_ts - # Store gaze position - ts_gaze_positions.append(gaze_position) + # Store gaze position + ts_gaze_positions.append(gaze_position) - return ts_gaze_positions + return ts_gaze_positions def build_gaze_saccade(size: int, center_A: tuple, center_B: tuple, min_time: float, max_time: float, start_ts: float = 0., validity: list = []): - """ Generate N TimeStampedGazePsoitions between 2 center points for testing purpose. - Timestamps are current time after random sleep (second). - GazePositions are random values. - """ - ts_gaze_positions = GazeFeatures.TimeStampedGazePositions() + """ Generate N TimeStampedGazePsoitions between 2 center points for testing purpose. + Timestamps are current time after random sleep (second). + GazePositions are random values. + """ + ts_gaze_positions = GazeFeatures.TimeStampedGazePositions() - start_time = time.time() + start_time = time.time() - for i in range(0, size): + for i in range(0, size): - # Sleep a random time - sleep_time = random.random() * (max_time - min_time) + min_time - time.sleep(sleep_time) + # Sleep a random time + sleep_time = random.random() * (max_time - min_time) + min_time + time.sleep(sleep_time) - # Check position validity - valid = True - if len(validity) > i: + # Check position validity + valid = True + if len(validity) > i: - valid = validity[i] + valid = validity[i] - if valid: + if valid: - # Edit gaze position - move_x = center_A[0] + (center_B[0] - center_A[0]) * (i / size) - move_y = center_A[1] + (center_B[1] - center_A[1]) * (i / size) - gaze_position = GazeFeatures.GazePosition((move_x, move_y)) + # Edit gaze position + move_x = center_A[0] + (center_B[0] - center_A[0]) * (i / size) + move_y = center_A[1] + (center_B[1] - center_A[1]) * (i / size) + gaze_position = GazeFeatures.GazePosition((move_x, move_y)) - else: + else: - gaze_position = GazeFeatures.GazePosition() + gaze_position = GazeFeatures.GazePosition() - # Timestamp gaze position - gaze_position.timestamp = time.time() - start_time + start_ts + # Timestamp gaze position + gaze_position.timestamp = time.time() - start_time + start_ts - # Store gaze position - ts_gaze_positions.append(gaze_position) + # Store gaze position + ts_gaze_positions.append(gaze_position) - return ts_gaze_positions + return ts_gaze_positions class TestVelocityThresholdIdentificationClass(unittest.TestCase): - """Test VelocityThresholdIdentification class.""" - - def test_fixation_identification(self): - """Test VelocityThresholdIdentification fixation identification.""" - - size = 10 - center = (0, 0) - deviation_max = 10 - min_time = 0.05 - max_time = 0.1 - velocity_max = deviation_max / min_time - - ts_gaze_positions = build_gaze_fixation(size, center, deviation_max, min_time, max_time) - gaze_movement_identifier = VelocityThresholdIdentification.GazeMovementIdentifier(velocity_max_threshold=velocity_max, duration_min_threshold=max_time*2) - ts_fixations, ts_saccades, ts_status = gaze_movement_identifier.browse(ts_gaze_positions) - - # Check result size - self.assertEqual(len(ts_fixations), 1) - self.assertEqual(len(ts_saccades), 0) - self.assertEqual(len(ts_status), size - 1) - - # Check fixation - fixation = ts_fixations.pop(0) - - self.assertEqual(len(fixation), size - 1) - self.assertGreaterEqual(fixation.duration, (size - 2) * min_time) - self.assertLessEqual(fixation.duration, (size - 2) * max_time) - self.assertLessEqual(fixation.is_finished(), True) - - def test_fixation_and_direct_saccade_identification(self): - """Test VelocityThresholdIdentification fixation and saccade identification.""" - - size = 10 - center_A = (0, 0) - center_B = (500, 500) - deviation_max = 10 - min_time = 0.05 - max_time = 0.1 - velocity_max = deviation_max / min_time - - ts_gaze_positions_A = build_gaze_fixation(size, center_A, deviation_max, min_time, max_time) - ts_gaze_positions_B = build_gaze_fixation(size, center_B, deviation_max, min_time, max_time, start_ts=ts_gaze_positions_A[-1].timestamp) - - ts_gaze_positions = ts_gaze_positions_A + ts_gaze_positions_B - - gaze_movement_identifier = VelocityThresholdIdentification.GazeMovementIdentifier(velocity_max_threshold=velocity_max, duration_min_threshold=max_time*2) - ts_fixations, ts_saccades, ts_status = gaze_movement_identifier.browse(ts_gaze_positions) - - # Check result size - self.assertEqual(len(ts_fixations), 2) - self.assertEqual(len(ts_saccades), 1) - self.assertEqual(len(ts_status), size * 2 - 1) - - # Check first fixation - fixation = ts_fixations.pop(0) - - self.assertEqual(len(fixation), size - 1) - self.assertGreaterEqual(fixation.duration, (size - 2) * min_time) - self.assertLessEqual(fixation.duration, (size - 2) * max_time) - self.assertLessEqual(fixation.is_finished(), True) - - # Check first saccade - saccade = ts_saccades.pop(0) - - self.assertEqual(len(saccade), 2) - self.assertGreaterEqual(saccade.duration, min_time) - self.assertLessEqual(saccade.duration, max_time) - self.assertLessEqual(saccade.is_finished(), True) - - # Check that last position of a movement is equal to first position of next movement - self.assertEqual(fixation[-1].timestamp, saccade[0].timestamp) - self.assertEqual(fixation[-1].value, saccade[0].value) - - # Check second fixation - fixation = ts_fixations.pop(0) - - self.assertEqual(len(fixation), size) - self.assertGreaterEqual(fixation.duration, (size - 1) * min_time) - self.assertLessEqual(fixation.duration, (size - 1) * max_time) - self.assertLessEqual(fixation.is_finished(), True) - - # Check that last position of a movement is equal to first position of next movement - self.assertEqual(saccade[-1].timestamp, fixation[0].timestamp) - self.assertEqual(saccade[-1].value, fixation[0].value) - - def test_fixation_and_short_saccade_identification(self): - """Test VelocityThresholdIdentification fixation and saccade identification.""" - - size = 10 - move = 2 - center_A = (0, 0) - out_A = (10, 10) - center_B = (50, 50) - deviation_max = 10 - min_time = 0.05 - max_time = 0.1 - velocity_max = deviation_max / min_time - - ts_gaze_positions_A = build_gaze_fixation(size, center_A, deviation_max, min_time, max_time) - ts_move_positions = build_gaze_saccade(move, out_A, center_B, min_time, min_time, start_ts=ts_gaze_positions_A[-1].timestamp) - ts_gaze_positions_B = build_gaze_fixation(size, center_B, deviation_max, min_time, max_time, start_ts=ts_move_positions[-1].timestamp) - - ts_gaze_positions = ts_gaze_positions_A + ts_move_positions + ts_gaze_positions_B - - gaze_movement_identifier = VelocityThresholdIdentification.GazeMovementIdentifier(velocity_max_threshold=velocity_max, duration_min_threshold=max_time*2) - ts_fixations, ts_saccades, ts_status = gaze_movement_identifier.browse(ts_gaze_positions) - - # Check result size - self.assertEqual(len(ts_fixations), 2) - self.assertEqual(len(ts_saccades), 1) - self.assertEqual(len(ts_status), 2 * size + move - 1) - - # Check first fixation - fixation = ts_fixations.pop(0) - - self.assertEqual(len(fixation), size - 1) # BUG: NOT ALWAYS TRUE !!! - self.assertGreaterEqual(fixation.duration, (size - 2) * min_time) - self.assertLessEqual(fixation.duration, (size - 2) * max_time) - self.assertLessEqual(fixation.is_finished(), True) - - # Check first saccade - saccade = ts_saccades.pop(0) - - self.assertEqual(len(saccade), move + 2) - self.assertGreaterEqual(saccade.duration, (move + 1) * min_time) - self.assertLessEqual(saccade.duration, (move + 1) * max_time) - self.assertLessEqual(saccade.is_finished(), True) - - # Check that last position of a movement is equal to first position of next movement - self.assertEqual(fixation[-1].timestamp, saccade[0].timestamp) - self.assertEqual(fixation[-1].value, saccade[0].value) - - # Check second fixation - fixation = ts_fixations.pop(0) - - self.assertEqual(len(fixation), size) - self.assertGreaterEqual(fixation.duration, (size - 1) * min_time) - self.assertLessEqual(fixation.duration, (size - 1) * max_time) - self.assertLessEqual(fixation.is_finished(), True) - - # Check that last position of a movement is equal to first position of next movement - self.assertEqual(saccade[-1], fixation[0]) - self.assertEqual(saccade[-1].value, fixation[0].value) - - def test_invalid_gaze_position(self): - """Test VelocityThresholdIdentification fixation and saccade identification with invalid gaze position.""" - - size = 15 - center = (0, 0) - deviation_max = 10 - min_time = 0.05 - max_time = 0.1 - velocity_max = deviation_max / min_time - validity = [True, True, True, True, True, True, True, False, False, False, True, True, True, True, True] - - ts_gaze_positions = build_gaze_fixation(size, center, deviation_max, min_time, max_time, validity=validity) - - gaze_movement_identifier = VelocityThresholdIdentification.GazeMovementIdentifier(velocity_max_threshold=velocity_max, duration_min_threshold=max_time*2) - ts_fixations, ts_saccades, ts_status = gaze_movement_identifier.browse(ts_gaze_positions) - - # Check result size - self.assertEqual(len(ts_fixations), 2) - self.assertEqual(len(ts_saccades), 0) - self.assertEqual(len(ts_status), len(validity)-5) - - # Check first fixation - fixation = ts_fixations.pop(0) - - self.assertEqual(len(fixation), 6) - self.assertGreaterEqual(fixation.duration, 5 * min_time) - self.assertLessEqual(fixation.duration, 5 * max_time) - self.assertLessEqual(fixation.is_finished(), True) + """Test VelocityThresholdIdentification class.""" + + def test_fixation_identification(self): + """Test VelocityThresholdIdentification fixation identification.""" + + size = 10 + center = (0, 0) + deviation_max = 10 + min_time = 0.05 + max_time = 0.1 + velocity_max = deviation_max / min_time + + ts_gaze_positions = build_gaze_fixation(size, center, deviation_max, min_time, max_time) + gaze_movement_identifier = VelocityThresholdIdentification.GazeMovementIdentifier(velocity_max_threshold=velocity_max, duration_min_threshold=max_time*2) + ts_fixations, ts_saccades, ts_status = gaze_movement_identifier.browse(ts_gaze_positions) + + # Check result size + self.assertEqual(len(ts_fixations), 1) + self.assertEqual(len(ts_saccades), 0) + self.assertEqual(len(ts_status), size - 1) + + # Check fixation + fixation = ts_fixations.pop(0) + + self.assertEqual(len(fixation), size - 1) + self.assertGreaterEqual(fixation.duration, (size - 2) * min_time) + self.assertLessEqual(fixation.duration, (size - 2) * max_time) + self.assertLessEqual(fixation.is_finished(), True) + + def test_fixation_and_direct_saccade_identification(self): + """Test VelocityThresholdIdentification fixation and saccade identification.""" + + size = 10 + center_A = (0, 0) + center_B = (500, 500) + deviation_max = 10 + min_time = 0.05 + max_time = 0.1 + velocity_max = deviation_max / min_time + + ts_gaze_positions_A = build_gaze_fixation(size, center_A, deviation_max, min_time, max_time) + ts_gaze_positions_B = build_gaze_fixation(size, center_B, deviation_max, min_time, max_time, start_ts=ts_gaze_positions_A[-1].timestamp) + + ts_gaze_positions = ts_gaze_positions_A + ts_gaze_positions_B + + gaze_movement_identifier = VelocityThresholdIdentification.GazeMovementIdentifier(velocity_max_threshold=velocity_max, duration_min_threshold=max_time*2) + ts_fixations, ts_saccades, ts_status = gaze_movement_identifier.browse(ts_gaze_positions) + + # Check result size + self.assertEqual(len(ts_fixations), 2) + self.assertEqual(len(ts_saccades), 1) + self.assertEqual(len(ts_status), size * 2 - 1) + + # Check first fixation + fixation = ts_fixations.pop(0) + + self.assertEqual(len(fixation), size - 1) + self.assertGreaterEqual(fixation.duration, (size - 2) * min_time) + self.assertLessEqual(fixation.duration, (size - 2) * max_time) + self.assertLessEqual(fixation.is_finished(), True) + + # Check first saccade + saccade = ts_saccades.pop(0) + + self.assertEqual(len(saccade), 2) + self.assertGreaterEqual(saccade.duration, min_time) + self.assertLessEqual(saccade.duration, max_time) + self.assertLessEqual(saccade.is_finished(), True) + + # Check that last position of a movement is equal to first position of next movement + self.assertEqual(fixation[-1].timestamp, saccade[0].timestamp) + self.assertEqual(fixation[-1].value, saccade[0].value) + + # Check second fixation + fixation = ts_fixations.pop(0) + + self.assertEqual(len(fixation), size) + self.assertGreaterEqual(fixation.duration, (size - 1) * min_time) + self.assertLessEqual(fixation.duration, (size - 1) * max_time) + self.assertLessEqual(fixation.is_finished(), True) + + # Check that last position of a movement is equal to first position of next movement + self.assertEqual(saccade[-1].timestamp, fixation[0].timestamp) + self.assertEqual(saccade[-1].value, fixation[0].value) + + def test_fixation_and_short_saccade_identification(self): + """Test VelocityThresholdIdentification fixation and saccade identification.""" + + size = 10 + move = 2 + center_A = (0, 0) + out_A = (10, 10) + center_B = (50, 50) + deviation_max = 10 + min_time = 0.05 + max_time = 0.1 + velocity_max = deviation_max / min_time + + ts_gaze_positions_A = build_gaze_fixation(size, center_A, deviation_max, min_time, max_time) + ts_move_positions = build_gaze_saccade(move, out_A, center_B, min_time, min_time, start_ts=ts_gaze_positions_A[-1].timestamp) + ts_gaze_positions_B = build_gaze_fixation(size, center_B, deviation_max, min_time, max_time, start_ts=ts_move_positions[-1].timestamp) + + ts_gaze_positions = ts_gaze_positions_A + ts_move_positions + ts_gaze_positions_B + + gaze_movement_identifier = VelocityThresholdIdentification.GazeMovementIdentifier(velocity_max_threshold=velocity_max, duration_min_threshold=max_time*2) + ts_fixations, ts_saccades, ts_status = gaze_movement_identifier.browse(ts_gaze_positions) + + # Check result size + self.assertEqual(len(ts_fixations), 2) + self.assertEqual(len(ts_saccades), 1) + self.assertEqual(len(ts_status), 2 * size + move - 1) + + # Check first fixation + fixation = ts_fixations.pop(0) + + self.assertEqual(len(fixation), size - 1) # BUG: NOT ALWAYS TRUE !!! + self.assertGreaterEqual(fixation.duration, (size - 2) * min_time) + self.assertLessEqual(fixation.duration, (size - 2) * max_time) + self.assertLessEqual(fixation.is_finished(), True) + + # Check first saccade + saccade = ts_saccades.pop(0) + + self.assertEqual(len(saccade), move + 2) + self.assertGreaterEqual(saccade.duration, (move + 1) * min_time) + self.assertLessEqual(saccade.duration, (move + 1) * max_time) + self.assertLessEqual(saccade.is_finished(), True) + + # Check that last position of a movement is equal to first position of next movement + self.assertEqual(fixation[-1].timestamp, saccade[0].timestamp) + self.assertEqual(fixation[-1].value, saccade[0].value) + + # Check second fixation + fixation = ts_fixations.pop(0) + + self.assertEqual(len(fixation), size) + self.assertGreaterEqual(fixation.duration, (size - 1) * min_time) + self.assertLessEqual(fixation.duration, (size - 1) * max_time) + self.assertLessEqual(fixation.is_finished(), True) + + # Check that last position of a movement is equal to first position of next movement + self.assertEqual(saccade[-1], fixation[0]) + self.assertEqual(saccade[-1].value, fixation[0].value) + + def test_invalid_gaze_position(self): + """Test VelocityThresholdIdentification fixation and saccade identification with invalid gaze position.""" + + size = 15 + center = (0, 0) + deviation_max = 10 + min_time = 0.05 + max_time = 0.1 + velocity_max = deviation_max / min_time + validity = [True, True, True, True, True, True, True, False, False, False, True, True, True, True, True] + + ts_gaze_positions = build_gaze_fixation(size, center, deviation_max, min_time, max_time, validity=validity) + + gaze_movement_identifier = VelocityThresholdIdentification.GazeMovementIdentifier(velocity_max_threshold=velocity_max, duration_min_threshold=max_time*2) + ts_fixations, ts_saccades, ts_status = gaze_movement_identifier.browse(ts_gaze_positions) + + # Check result size + self.assertEqual(len(ts_fixations), 2) + self.assertEqual(len(ts_saccades), 0) + self.assertEqual(len(ts_status), len(validity)-5) + + # Check first fixation + fixation = ts_fixations.pop(0) + + self.assertEqual(len(fixation), 6) + self.assertGreaterEqual(fixation.duration, 5 * min_time) + self.assertLessEqual(fixation.duration, 5 * max_time) + self.assertLessEqual(fixation.is_finished(), True) - # Check second fixation - fixation = ts_fixations.pop(0) - - self.assertEqual(len(fixation), 4) - self.assertGreaterEqual(fixation.duration, 3 * min_time) - self.assertLessEqual(fixation.duration, 3 * max_time) - self.assertLessEqual(fixation.is_finished(), True) + # Check second fixation + fixation = ts_fixations.pop(0) + + self.assertEqual(len(fixation), 4) + self.assertGreaterEqual(fixation.duration, 3 * min_time) + self.assertLessEqual(fixation.duration, 3 * max_time) + self.assertLessEqual(fixation.is_finished(), True) - def test_identification_browsing(self): - """Test VelocityThresholdIdentification identification browsing.""" - - size = 10 - center_A = (0, 0) - center_B = (50, 50) - deviation_max = 10 - min_time = 0.01 - max_time = 0.1 - velocity_max = deviation_max / min_time - - ts_gaze_positions_A = build_gaze_fixation(size, center_A, deviation_max, min_time, max_time) - ts_gaze_positions_B = build_gaze_fixation(size, center_B, deviation_max, min_time, max_time, start_ts=ts_gaze_positions_A[-1].timestamp) + def test_identification_browsing(self): + """Test VelocityThresholdIdentification identification browsing.""" + + size = 10 + center_A = (0, 0) + center_B = (50, 50) + deviation_max = 10 + min_time = 0.01 + max_time = 0.1 + velocity_max = deviation_max / min_time + + ts_gaze_positions_A = build_gaze_fixation(size, center_A, deviation_max, min_time, max_time) + ts_gaze_positions_B = build_gaze_fixation(size, center_B, deviation_max, min_time, max_time, start_ts=ts_gaze_positions_A[-1].timestamp) - ts_gaze_positions = ts_gaze_positions_A + ts_gaze_positions_B + ts_gaze_positions = ts_gaze_positions_A + ts_gaze_positions_B - gaze_movement_identifier = VelocityThresholdIdentification.GazeMovementIdentifier(velocity_max_threshold=velocity_max, duration_min_threshold=max_time*2) + gaze_movement_identifier = VelocityThresholdIdentification.GazeMovementIdentifier(velocity_max_threshold=velocity_max, duration_min_threshold=max_time*2) - # Iterate on gaze positions - for gaze_position in ts_gaze_positions: + # Iterate on gaze positions + for gaze_position in ts_gaze_positions: - finished_gaze_movement = gaze_movement_identifier.identify(gaze_position, terminate=(gaze_position.timestamp == ts_gaze_positions[-1])) + finished_gaze_movement = gaze_movement_identifier.identify(gaze_position, terminate=(gaze_position.timestamp == ts_gaze_positions[-1])) - # Check that last gaze position date is not equal to given gaze position date - if finished_gaze_movement: + # Check that last gaze position date is not equal to given gaze position date + if finished_gaze_movement: - self.assertNotEqual(finished_gaze_movement[-1].timestamp, gaze_position.timestamp) + 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() - if current_gaze_movement: + # 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() + if current_gaze_movement: - self.assertEqual(current_gaze_movement[-1].timestamp, gaze_position.timestamp) + self.assertEqual(current_gaze_movement[-1].timestamp, gaze_position.timestamp) if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() \ No newline at end of file diff --git a/src/argaze/ArFeatures.py b/src/argaze/ArFeatures.py index 2d9c281..aaac6ed 100644 --- a/src/argaze/ArFeatures.py +++ b/src/argaze/ArFeatures.py @@ -96,7 +96,7 @@ class ArLayer(DataFeatures.SharedObject, DataFeatures.PipelineStepObject): Defines a space where to make matching of gaze movements and AOI and inside which those matching need to be analyzed. !!! note - Inherits from DataFeatures.SharedObject class to be shared by multiple threads. + Inherits from DataFeatures.SharedObject class to be shared by multiple threads. """ @DataFeatures.PipelineStepInit @@ -320,7 +320,7 @@ class ArLayer(DataFeatures.SharedObject, DataFeatures.PipelineStepObject): Project timestamped gaze movement into layer. !!! warning - Be aware that gaze movement positions are in the same range of value than aoi_scene size attribute. + Be aware that gaze movement positions are in the same range of value than aoi_scene size attribute. Parameters: gaze_movement: gaze movement to project @@ -435,7 +435,7 @@ class ArFrame(DataFeatures.SharedObject, DataFeatures.PipelineStepObject): Defines a rectangular area where to project in timestamped gaze positions and inside which they need to be analyzed. !!! note - Inherits from DataFeatures.SharedObject class to be shared by multiple threads + Inherits from DataFeatures.SharedObject class to be shared by multiple threads """ @DataFeatures.PipelineStepInit @@ -703,7 +703,7 @@ class ArFrame(DataFeatures.SharedObject, DataFeatures.PipelineStepObject): Project timestamped gaze position into frame. !!! warning - Be aware that gaze positions are in the same range of value than size attribute. + Be aware that gaze positions are in the same range of value than size attribute. Parameters: timestamped_gaze_position: gaze position to project @@ -1305,10 +1305,10 @@ class ArCamera(ArFrame): """Copy camera frame background into scene frames background. !!! warning - This method have to be called once AOI have been projected into camera frame layers. + This method have to be called once AOI have been projected into camera frame layers. !!! note - This method makes each frame to send an 'on_copy_background_into_scenes_frames' signal to their observers. + This method makes each frame to send an 'on_copy_background_into_scenes_frames' signal to their observers. """ # Project camera frame background into each scene frame if possible @@ -1445,7 +1445,7 @@ class ArCamera(ArFrame): """Project timestamped gaze position into each scene frames. !!! warning - watch method needs to be called first. + watch method needs to be called first. Parameters: timestamped_gaze_position: gaze position to project @@ -1504,7 +1504,7 @@ DEFAULT_ARCONTEXT_IMAGE_PARAMETERS = { class ArContext(DataFeatures.PipelineStepObject): """ - Defines abstract Python context manager to handle pipeline inputs. + Defines abstract Python context manager to handle incoming gaze data before passing them to a processing pipeline. """ # noinspection PyMissingConstructor diff --git a/src/argaze/ArUcoMarker/ArUcoDetector.py b/src/argaze/ArUcoMarker/ArUcoDetector.py index 32091a4..50da144 100644 --- a/src/argaze/ArUcoMarker/ArUcoDetector.py +++ b/src/argaze/ArUcoMarker/ArUcoDetector.py @@ -33,7 +33,7 @@ class DetectorParameters(): """Wrapper class around ArUco marker detector parameters. !!! note - More details on [opencv page](https://docs.opencv.org/4.x/d1/dcd/structcv_1_1aruco_1_1DetectorParameters.html) + More details on [opencv page](https://docs.opencv.org/4.x/d1/dcd/structcv_1_1aruco_1_1DetectorParameters.html) """ __parameters = aruco.DetectorParameters() @@ -188,13 +188,13 @@ class ArUcoDetector(DataFeatures.PipelineStepObject): """Detect all ArUco markers into an image. !!! danger "DON'T MIRROR IMAGE" - It makes the markers detection to fail. + It makes the markers detection to fail. !!! danger "DON'T UNDISTORTED IMAGE" - Camera intrinsic parameters and distortion coefficients are used later during pose estimation. + Camera intrinsic parameters and distortion coefficients are used later during pose estimation. !!! note - The pose of markers will be also estimated if the pose_size attribute is not None. + The pose of markers will be also estimated if the pose_size attribute is not None. """ # Reset detected markers data @@ -262,7 +262,7 @@ class ArUcoDetector(DataFeatures.PipelineStepObject): ids: markers id list to select detected markers. !!! warning - This method have to called after 'detect_markers' + This method have to called after 'detect_markers' """ # Is there detected markers ? @@ -324,7 +324,7 @@ class ArUcoDetector(DataFeatures.PipelineStepObject): """Detect ArUco markers board in image setting up the number of detected markers needed to agree detection. !!! danger "DON'T MIRROR IMAGE" - It makes the markers detection to fail. + It makes the markers detection to fail. """ # detect markers from gray picture diff --git a/src/argaze/ArUcoMarker/ArUcoMarker.py b/src/argaze/ArUcoMarker/ArUcoMarker.py index bfd6350..fddc2aa 100644 --- a/src/argaze/ArUcoMarker/ArUcoMarker.py +++ b/src/argaze/ArUcoMarker/ArUcoMarker.py @@ -28,80 +28,80 @@ import cv2.aruco as aruco @dataclass class ArUcoMarker(): - """Define ArUco marker class.""" + """Define ArUco marker class.""" - dictionary: ArUcoMarkerDictionary.ArUcoMarkerDictionary - """Dictionary to which it belongs.""" + dictionary: ArUcoMarkerDictionary.ArUcoMarkerDictionary + """Dictionary to which it belongs.""" - identifier: int - """Index into dictionary""" + identifier: int + """Index into dictionary""" - size: float = field(default=math.nan) - """Size of marker in centimeters.""" + size: float = field(default=math.nan) + """Size of marker in centimeters.""" - corners: numpy.array = field(init=False, repr=False) - """Estimated 2D corners position in camera image referential.""" + corners: numpy.array = field(init=False, repr=False) + """Estimated 2D corners position in camera image referential.""" - translation: numpy.array = field(init=False, repr=False) - """Estimated 3D center position in camera world referential.""" + translation: numpy.array = field(init=False, repr=False) + """Estimated 3D center position in camera world referential.""" - rotation: numpy.array = field(init=False, repr=False) - """Estimated 3D marker rotation in camera world referential.""" + rotation: numpy.array = field(init=False, repr=False) + """Estimated 3D marker rotation in camera world referential.""" - points: numpy.array = field(init=False, repr=False) - """Estimated 3D corners positions in camera world referential.""" + points: numpy.array = field(init=False, repr=False) + """Estimated 3D corners positions in camera world referential.""" - @property - def center(self) -> numpy.array: - """Get 2D center position in camera image referential.""" + @property + def center(self) -> numpy.array: + """Get 2D center position in camera image referential.""" - return self.corners[0].mean(axis=0) + return self.corners[0].mean(axis=0) - def image(self, dpi) -> numpy.array: - """Create marker matrix image at a given resolution. + def image(self, dpi) -> numpy.array: + """Create marker matrix image at a given resolution. - !!! warning - Marker size have to be setup before. - """ + !!! warning + Marker size have to be setup before. + """ - assert(not math.isnan(self.size)) + assert(not math.isnan(self.size)) - dimension = round(self.size * dpi / 2.54) # 1 cm = 2.54 inches - matrix = numpy.zeros((dimension, dimension, 1), dtype="uint8") + dimension = round(self.size * dpi / 2.54) # 1 cm = 2.54 inches + matrix = numpy.zeros((dimension, dimension, 1), dtype="uint8") - aruco.generateImageMarker(self.dictionary.markers, self.identifier, dimension, matrix, 1) + aruco.generateImageMarker(self.dictionary.markers, self.identifier, dimension, matrix, 1) - return numpy.repeat(matrix, 3).reshape(dimension, dimension, 3) + return numpy.repeat(matrix, 3).reshape(dimension, dimension, 3) - def draw(self, image: numpy.array, K: numpy.array, D: numpy.array, color: tuple = None, draw_axes: dict = None): - """Draw marker in image. + def draw(self, image: numpy.array, K: numpy.array, D: numpy.array, color: tuple = None, draw_axes: dict = None): + """Draw marker in image. - Parameters: - image: image where to - K: - D: - color: marker color (if None, no marker drawn) - draw_axes: enable marker axes drawing + Parameters: + image: image where to + K: + D: + color: marker color (if None, no marker drawn) + draw_axes: enable marker axes drawing - !!! warning - draw_axes needs marker size and pose estimation. - """ + !!! warning + draw_axes needs marker size and pose estimation. + """ - # Draw marker if required - if color is not None: + # Draw marker if required + if color is not None: - aruco.drawDetectedMarkers(image, [numpy.array([list(self.corners)])], numpy.array([self.identifier]), color) + aruco.drawDetectedMarkers(image, [numpy.array([list(self.corners)])], numpy.array([self.identifier]), color) - # Draw marker axes if pose has been estimated, marker have a size and if required - if self.translation.size == 3 and self.rotation.size == 9 and not math.isnan(self.size) and draw_axes is not None: + # Draw marker axes if pose has been estimated, marker have a size and if required + if self.translation.size == 3 and self.rotation.size == 9 and not math.isnan(self.size) and draw_axes is not None: - cv2.drawFrameAxes(image, numpy.array(K), numpy.array(D), self.rotation, self.translation, self.size, **draw_axes) + cv2.drawFrameAxes(image, numpy.array(K), numpy.array(D), self.rotation, self.translation, self.size, **draw_axes) - def save(self, destination_folder, dpi): - """Save marker image as .png file into a destination folder.""" + def save(self, destination_folder, dpi): + """Save marker image as .png file into a destination folder.""" - filename = f'{self.dictionary.name}_{self.dictionary.format}_{self.identifier}.png' - filepath = f'{destination_folder}/{filename}' + filename = f'{self.dictionary.name}_{self.dictionary.format}_{self.identifier}.png' + filepath = f'{destination_folder}/{filename}' - cv2.imwrite(filepath, self.image(dpi)) + cv2.imwrite(filepath, self.image(dpi)) diff --git a/src/argaze/ArUcoMarker/ArUcoMarkerGroup.py b/src/argaze/ArUcoMarker/ArUcoMarkerGroup.py index 1cca6c4..5575cad 100644 --- a/src/argaze/ArUcoMarker/ArUcoMarkerGroup.py +++ b/src/argaze/ArUcoMarker/ArUcoMarkerGroup.py @@ -206,11 +206,10 @@ class ArUcoMarkerGroup(DataFeatures.PipelineStepObject): """Load ArUco markers group from .obj file. !!! note - Expected object (o) name format: #_Marker + Expected object (o) name format: #_Marker !!! note - All markers have to belong to the same dictionary. - + All markers have to belong to the same dictionary. """ new_dictionary = None diff --git a/src/argaze/AreaOfInterest/AOI2DScene.py b/src/argaze/AreaOfInterest/AOI2DScene.py index 9c74637..b19c6e9 100644 --- a/src/argaze/AreaOfInterest/AOI2DScene.py +++ b/src/argaze/AreaOfInterest/AOI2DScene.py @@ -43,10 +43,10 @@ class AOI2DScene(AOIFeatures.AOIScene): svg_filepath: path to svg file !!! note - Available SVG elements are: path, rect and circle. + Available SVG elements are: path, rect and circle. !!! warning - Available SVG path d-string commands are: MoveTo (M) LineTo (L) and ClosePath (Z) commands. + Available SVG path d-string commands are: MoveTo (M) LineTo (L) and ClosePath (Z) commands. """ with minidom.parse(svg_filepath) as description_file: diff --git a/src/argaze/AreaOfInterest/AOI3DScene.py b/src/argaze/AreaOfInterest/AOI3DScene.py index 13ea354..232329c 100644 --- a/src/argaze/AreaOfInterest/AOI3DScene.py +++ b/src/argaze/AreaOfInterest/AOI3DScene.py @@ -179,8 +179,8 @@ class AOI3DScene(AOIFeatures.AOIScene): """Get AOI which are inside and out a given cone field. !!! note - **By default** - The cone have its tip at origin and its base oriented to positive Z axis. + **By default** + The cone have its tip at origin and its base oriented to positive Z axis. Returns: scene inside the cone @@ -226,11 +226,11 @@ class AOI3DScene(AOIFeatures.AOIScene): D: camera distortion coefficients vector !!! danger - Camera distortion coefficients could project points which are far from image frame into it. + Camera distortion coefficients could project points which are far from image frame into it. !!! note - As gaze is mainly focusing on frame center, where the distortion is low, - it could be acceptable to not use camera distortion. + As gaze is mainly focusing on frame center, where the distortion is low, + it could be acceptable to not use camera distortion. """ aoi2D_scene = AOI2DScene.AOI2DScene() diff --git a/src/argaze/AreaOfInterest/AOIFeatures.py b/src/argaze/AreaOfInterest/AOIFeatures.py index 25046ff..fb61f61 100644 --- a/src/argaze/AreaOfInterest/AOIFeatures.py +++ b/src/argaze/AreaOfInterest/AOIFeatures.py @@ -143,7 +143,7 @@ class AreaOfInterest(numpy.ndarray): def bounding_box(self) -> numpy.array: """Get area's bounding box. !!! warning - Available for 2D AOI only.""" + Available for 2D AOI only.""" assert (self.points_number > 1) assert (self.dimension == 2) @@ -162,7 +162,7 @@ class AreaOfInterest(numpy.ndarray): def clockwise(self) -> Self: """Get area points in clockwise order. !!! warning - Available for 2D AOI only.""" + Available for 2D AOI only.""" assert (self.dimension == 2) @@ -175,9 +175,9 @@ class AreaOfInterest(numpy.ndarray): def contains_point(self, point: tuple) -> bool: """Is a point inside area? !!! warning - Available for 2D AOI only. + Available for 2D AOI only. !!! danger - The AOI points must be sorted in clockwise order.""" + The AOI points must be sorted in clockwise order.""" assert (self.dimension == 2) assert (len(point) == self.dimension) @@ -187,9 +187,9 @@ class AreaOfInterest(numpy.ndarray): def inner_axis(self, x: float, y: float) -> tuple: """Transform a point coordinates from global axis to AOI axis. !!! warning - Available for 2D AOI only. + Available for 2D AOI only. !!! danger - The AOI points must be sorted in clockwise order.""" + The AOI points must be sorted in clockwise order.""" assert (self.dimension == 2) @@ -210,9 +210,9 @@ class AreaOfInterest(numpy.ndarray): def outter_axis(self, x: float, y: float) -> tuple: """Transform a point coordinates from AOI axis to global axis. !!! danger - The AOI points must be sorted in clockwise order. + The AOI points must be sorted in clockwise order. !!! danger - The AOI must be a rectangle. + The AOI must be a rectangle. """ # Origin point @@ -230,7 +230,7 @@ class AreaOfInterest(numpy.ndarray): def circle_intersection(self, center: tuple, radius: float) -> tuple[numpy.array, float, float]: """Get intersection shape with a circle, intersection area / AOI area ratio and intersection area / circle area ratio. !!! warning - Available for 2D AOI only. + Available for 2D AOI only. Returns: intersection shape @@ -267,7 +267,7 @@ class AreaOfInterest(numpy.ndarray): def draw(self, image: numpy.array, color, border_size=1): """Draw 2D AOI into image. !!! warning - Available for 2D AOI only.""" + Available for 2D AOI only.""" assert (self.dimension == 2) diff --git a/src/argaze/DataFeatures.py b/src/argaze/DataFeatures.py index 60e382b..2629e8e 100644 --- a/src/argaze/DataFeatures.py +++ b/src/argaze/DataFeatures.py @@ -134,7 +134,7 @@ def from_json(filepath: str) -> any: Load object from json file. !!! note - The directory where json file is will be used as global working directory. + The directory where json file is will be used as global working directory. Parameters: filepath: path to json file @@ -354,8 +354,7 @@ class TimestampedObjectsList(list): """Handle timestamped object into a list. !!! warning "Timestamped objects are not sorted internally" - - Timestamped objects are considered to be stored according to their coming time. + Timestamped objects are considered to be stored according to their coming time. """ # noinspection PyMissingConstructor @@ -450,12 +449,10 @@ class TimestampedObjectsList(list): For example: to convert {"point": (0, 0)} data as two separated "x" and "y" columns, use split={"point": ["x", "y"]} !!! warning "Values must be dictionaries" - - Each key is stored as a column name. + Each key is stored as a column name. !!! note - - Timestamps are stored as index column called 'timestamp'. + Timestamps are stored as index column called 'timestamp'. """ df = pandas.DataFrame(self.tuples(), columns=self.__object_properties_names) diff --git a/src/argaze/GazeAnalysis/DeviationCircleCoverage.py b/src/argaze/GazeAnalysis/DeviationCircleCoverage.py index 3d910c7..3bf8f46 100644 --- a/src/argaze/GazeAnalysis/DeviationCircleCoverage.py +++ b/src/argaze/GazeAnalysis/DeviationCircleCoverage.py @@ -26,195 +26,195 @@ from argaze.GazeAnalysis import DispersionThresholdIdentification, VelocityThres 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.""" - @DataFeatures.PipelineStepInit - def __init__(self, **kwargs): + @DataFeatures.PipelineStepInit + def __init__(self, **kwargs): - # Init AOIMatcher class - super().__init__() + # Init AOIMatcher class + super().__init__() - self.__coverage_threshold = 0 + self.__coverage_threshold = 0 - self.__reset() + self.__reset() - @property - def coverage_threshold(self) -> float: - """Minimal coverage ratio to consider a fixation over an AOI (1 means that whole fixation's deviation circle have to be over the AOI).""" - return self.__coverage_threshold + @property + def coverage_threshold(self) -> float: + """Minimal coverage ratio to consider a fixation over an AOI (1 means that whole fixation's deviation circle have to be over the AOI).""" + return self.__coverage_threshold - @coverage_threshold.setter - def coverage_threshold(self, coverage_threshold: float): + @coverage_threshold.setter + def coverage_threshold(self, coverage_threshold: float): - self.__coverage_threshold = coverage_threshold - - def __reset(self): + self.__coverage_threshold = coverage_threshold + + def __reset(self): - self.__look_count = 0 - self.__looked_aoi_data = (None, None) - self.__looked_probabilities = {} - self.__circle_ratio_sum = {} - self.__matched_gaze_movement = None - self.__matched_region = None + self.__look_count = 0 + self.__looked_aoi_data = (None, None) + self.__looked_probabilities = {} + self.__circle_ratio_sum = {} + self.__matched_gaze_movement = None + self.__matched_region = None - @DataFeatures.PipelineStepMethod - def match(self, gaze_movement: GazeFeatures.GazeMovement, aoi_scene) -> tuple[str, AOIFeatures.AreaOfInterest]: - """Returns AOI with the maximal fixation's deviation circle coverage if above coverage threshold.""" + @DataFeatures.PipelineStepMethod + def match(self, gaze_movement: GazeFeatures.GazeMovement, aoi_scene) -> tuple[str, AOIFeatures.AreaOfInterest]: + """Returns AOI with the maximal fixation's deviation circle coverage if above coverage threshold.""" - if GazeFeatures.is_fixation(gaze_movement): + if GazeFeatures.is_fixation(gaze_movement): - self.__look_count += 1 + self.__look_count += 1 - max_coverage = 0. - most_likely_looked_aoi_data = (None, None) - matched_region = None + max_coverage = 0. + most_likely_looked_aoi_data = (None, None) + matched_region = None - for name, aoi in aoi_scene.items(): + for name, aoi in aoi_scene.items(): - # DispersionThresholdIdentification.Fixation: use maximal deviation - if issubclass(type(gaze_movement), DispersionThresholdIdentification.Fixation): + # DispersionThresholdIdentification.Fixation: use maximal deviation + if issubclass(type(gaze_movement), DispersionThresholdIdentification.Fixation): - fixation_circle_radius = gaze_movement.deviation_max + fixation_circle_radius = gaze_movement.deviation_max - # VelocityThresholdIdentification.Fixation: use amplitude - elif issubclass(type(gaze_movement), VelocityThresholdIdentification.Fixation): + # VelocityThresholdIdentification.Fixation: use amplitude + elif issubclass(type(gaze_movement), VelocityThresholdIdentification.Fixation): - fixation_circle_radius = gaze_movement.amplitude + fixation_circle_radius = gaze_movement.amplitude - # Otherwise, compute maximal deviation - else: + # Otherwise, compute maximal deviation + else: - fixation_circle_radius = max(gaze_movement.distances(gaze_movement.focus)) + fixation_circle_radius = max(gaze_movement.distances(gaze_movement.focus)) - # Intersect - region, _, circle_ratio = aoi.circle_intersection(gaze_movement.focus, fixation_circle_radius) + # Intersect + region, _, circle_ratio = aoi.circle_intersection(gaze_movement.focus, fixation_circle_radius) - 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: + # Sum circle ratio to update aoi coverage + try: - self.__circle_ratio_sum[name] += circle_ratio + self.__circle_ratio_sum[name] += circle_ratio - except KeyError: + except KeyError: - self.__circle_ratio_sum[name] = circle_ratio + self.__circle_ratio_sum[name] = circle_ratio - # Update maximal coverage and most likely looked aoi - if self.__circle_ratio_sum[name] > max_coverage: + # Update maximal coverage and most likely looked aoi + if self.__circle_ratio_sum[name] > max_coverage: - max_coverage = self.__circle_ratio_sum[name] - most_likely_looked_aoi_data = (name, aoi) - matched_region = region - - # Check that aoi coverage happens - if max_coverage > 0: + max_coverage = self.__circle_ratio_sum[name] + most_likely_looked_aoi_data = (name, aoi) + matched_region = region + + # Check that aoi coverage happens + if max_coverage > 0: - # Update looked aoi data - # noinspection PyAttributeOutsideInit - self.__looked_aoi_data = most_likely_looked_aoi_data + # Update looked aoi data + # noinspection PyAttributeOutsideInit + self.__looked_aoi_data = most_likely_looked_aoi_data - # Calculate circle ratio means as looked probabilities - # noinspection PyAttributeOutsideInit - self.__looked_probabilities = {} + # Calculate circle ratio means as looked probabilities + # noinspection PyAttributeOutsideInit + self.__looked_probabilities = {} - for aoi_name, circle_ratio_sum in self.__circle_ratio_sum.items(): + for aoi_name, circle_ratio_sum in self.__circle_ratio_sum.items(): - circle_ratio_mean = circle_ratio_sum / self.__look_count + circle_ratio_mean = circle_ratio_sum / self.__look_count - # Avoid probability greater than 1 - self.__looked_probabilities[aoi_name] = circle_ratio_mean if circle_ratio_mean < 1 else 1 + # Avoid probability greater than 1 + self.__looked_probabilities[aoi_name] = circle_ratio_mean if circle_ratio_mean < 1 else 1 - # Update matched gaze movement - # noinspection PyAttributeOutsideInit - self.__matched_gaze_movement = gaze_movement + # Update matched gaze movement + # noinspection PyAttributeOutsideInit + self.__matched_gaze_movement = gaze_movement - # Update matched region - # noinspection PyAttributeOutsideInit - self.__matched_region = matched_region + # Update matched region + # noinspection PyAttributeOutsideInit + self.__matched_region = matched_region - # Return - return self.__looked_aoi_data + # Return + return self.__looked_aoi_data - elif GazeFeatures.is_saccade(gaze_movement): + elif GazeFeatures.is_saccade(gaze_movement): - self.__reset() + self.__reset() - elif not gaze_movement: + elif not gaze_movement: - self.__reset() + self.__reset() - return (None, None) + return (None, None) - def draw(self, image: numpy.array, aoi_scene: AOIFeatures.AOIScene, draw_matched_fixation: dict = None, draw_matched_region: dict = None, draw_looked_aoi: dict = None, update_looked_aoi: bool = False, looked_aoi_name_color: tuple = None, looked_aoi_name_offset: tuple = (0, 0)): - """Draw matching into image. - - Parameters: - image: where to draw - aoi_scene: to refresh looked aoi if required - draw_matched_fixation: Fixation.draw parameters (which depends on the loaded - gaze movement identifier module, if None, no fixation is drawn) - draw_matched_region: AOIFeatures.AOI.draw parameters (if None, no matched region is drawn) - draw_looked_aoi: AOIFeatures.AOI.draw parameters (if None, no looked aoi is drawn) - update_looked_aoi: - looked_aoi_name_color: color of text (if None, no looked aoi name is drawn) - looked_aoi_name_offset: offset of text from the upper left aoi bounding box corner - """ + def draw(self, image: numpy.array, aoi_scene: AOIFeatures.AOIScene, draw_matched_fixation: dict = None, draw_matched_region: dict = None, draw_looked_aoi: dict = None, update_looked_aoi: bool = False, looked_aoi_name_color: tuple = None, looked_aoi_name_offset: tuple = (0, 0)): + """Draw matching into image. + + Parameters: + image: where to draw + aoi_scene: to refresh looked aoi if required + draw_matched_fixation: Fixation.draw parameters (which depends on the loaded + gaze movement identifier module, if None, no fixation is drawn) + draw_matched_region: AOIFeatures.AOI.draw parameters (if None, no matched region is drawn) + draw_looked_aoi: AOIFeatures.AOI.draw parameters (if None, no looked aoi is drawn) + update_looked_aoi: + looked_aoi_name_color: color of text (if None, no looked aoi name is drawn) + looked_aoi_name_offset: offset of text from the upper left aoi bounding box corner + """ - if self.__matched_gaze_movement is not None: + if self.__matched_gaze_movement is not None: - if GazeFeatures.is_fixation(self.__matched_gaze_movement): + if GazeFeatures.is_fixation(self.__matched_gaze_movement): - # Draw matched fixation if required - if draw_matched_fixation is not None: + # Draw matched fixation if required + if draw_matched_fixation is not None: - self.__matched_gaze_movement.draw(image, **draw_matched_fixation) - - # Draw matched aoi - if self.looked_aoi().all() is not None: + self.__matched_gaze_movement.draw(image, **draw_matched_fixation) + + # Draw matched aoi + if self.looked_aoi().all() is not None: - if update_looked_aoi: + if update_looked_aoi: - try: + try: - # noinspection PyAttributeOutsideInit - self.__looked_aoi_data = (self.looked_aoi_name(), aoi_scene[self.looked_aoi_name()]) + # noinspection PyAttributeOutsideInit + self.__looked_aoi_data = (self.looked_aoi_name(), aoi_scene[self.looked_aoi_name()]) - except KeyError: + except KeyError: - pass + pass - # Draw looked aoi if required - if draw_looked_aoi 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 matched region if required - if draw_matched_region is not None: + # Draw matched region if required + if draw_matched_region is not None: - self.__matched_region.draw(image, **draw_matched_region) + self.__matched_region.draw(image, **draw_matched_region) - # Draw looked aoi name if required - if looked_aoi_name_color is not None: + # 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) - def looked_aoi(self) -> AOIFeatures.AreaOfInterest: - """Get most likely looked aoi for current fixation (e.g. the aoi with the highest coverage mean value)""" + 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] + return self.__looked_aoi_data[1] - 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)""" + 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] + return self.__looked_aoi_data[0] - def looked_probabilities(self) -> dict: - """Get probabilities to be looked by current fixation for each aoi. + def looked_probabilities(self) -> dict: + """Get probabilities to be looked by current fixation for each aoi. - !!! note - aoi where fixation deviation circle never passed the coverage threshold will be missing. - """ + !!! note + aoi where fixation deviation circle never passed the coverage threshold will be missing. + """ - return self.__looked_probabilities \ No newline at end of file + return self.__looked_probabilities \ No newline at end of file diff --git a/src/argaze/GazeAnalysis/Entropy.py b/src/argaze/GazeAnalysis/Entropy.py index dfed82f..9d45d1d 100644 --- a/src/argaze/GazeAnalysis/Entropy.py +++ b/src/argaze/GazeAnalysis/Entropy.py @@ -24,79 +24,79 @@ from argaze.GazeAnalysis import TransitionMatrix class AOIScanPathAnalyzer(GazeFeatures.AOIScanPathAnalyzer): - """Implementation of entropy algorithm as described in: + """Implementation of entropy algorithm as described in: - **Krejtz K., Szmidt T., Duchowski A.T. (2014).** - *Entropy-based statistical analysis of eye movement transitions.* - Proceedings of the Symposium on Eye Tracking Research and Applications (ETRA'14, 159-166). - [https://doi.org/10.1145/2578153.2578176](https://doi.org/10.1145/2578153.2578176) - """ + **Krejtz K., Szmidt T., Duchowski A.T. (2014).** + *Entropy-based statistical analysis of eye movement transitions.* + Proceedings of the Symposium on Eye Tracking Research and Applications (ETRA'14, 159-166). + [https://doi.org/10.1145/2578153.2578176](https://doi.org/10.1145/2578153.2578176) + """ - @DataFeatures.PipelineStepInit - def __init__(self, **kwargs): + @DataFeatures.PipelineStepInit + def __init__(self, **kwargs): - # Init AOIScanPathAnalyzer class - super().__init__() + # Init AOIScanPathAnalyzer class + super().__init__() - self.__transition_matrix_analyzer = None - self.__stationary_entropy = -1 - self.__transition_entropy = -1 + self.__transition_matrix_analyzer = None + self.__stationary_entropy = -1 + self.__transition_entropy = -1 - @property - def transition_matrix_analyzer(self) -> TransitionMatrix.AOIScanPathAnalyzer: - """Bind to TransitionMatrix analyzer to get its transition_matrix_probabilities. + @property + def transition_matrix_analyzer(self) -> TransitionMatrix.AOIScanPathAnalyzer: + """Bind to TransitionMatrix analyzer to get its transition_matrix_probabilities. - !!! warning "Mandatory" - TransitionMatrix analyzer have to be loaded before. - """ + !!! warning "Mandatory" + TransitionMatrix analyzer have to be loaded before. + """ - return self.__transition_matrix_analyzer + return self.__transition_matrix_analyzer - @transition_matrix_analyzer.setter - def transition_matrix_analyzer(self, transition_matrix_analyzer: TransitionMatrix.AOIScanPathAnalyzer): + @transition_matrix_analyzer.setter + def transition_matrix_analyzer(self, transition_matrix_analyzer: TransitionMatrix.AOIScanPathAnalyzer): - self.__transition_matrix_analyzer = transition_matrix_analyzer + self.__transition_matrix_analyzer = transition_matrix_analyzer - @DataFeatures.PipelineStepMethod - def analyze(self, aoi_scan_path: GazeFeatures.AOIScanPath): + @DataFeatures.PipelineStepMethod + def analyze(self, aoi_scan_path: GazeFeatures.AOIScanPath): - assert(len(aoi_scan_path) > 1) + assert(len(aoi_scan_path) > 1) - # Count total number of fixations and how many fixations are there per aoi - scan_fixations_count, aoi_fixations_count = aoi_scan_path.fixations_count() + # Count total number of fixations and how many fixations are there per aoi + scan_fixations_count, aoi_fixations_count = aoi_scan_path.fixations_count() - # Probability to have a fixation onto each aoi - stationary_probabilities = {aoi: count/scan_fixations_count for aoi, count in aoi_fixations_count.items()} + # Probability to have a fixation onto each aoi + stationary_probabilities = {aoi: count/scan_fixations_count for aoi, count in aoi_fixations_count.items()} - # Stationary entropy - self.__stationary_entropy = 0 + # Stationary entropy + self.__stationary_entropy = 0 - for aoi, p in stationary_probabilities.items(): + for aoi, p in stationary_probabilities.items(): - self.__stationary_entropy += p * numpy.log(p + 1e-9) + self.__stationary_entropy += p * numpy.log(p + 1e-9) - self.__stationary_entropy *= -1 + self.__stationary_entropy *= -1 - # Transition entropy - self.__transition_entropy = 0 + # Transition entropy + self.__transition_entropy = 0 - destination_p_log_sum = self.transition_matrix_analyzer.transition_matrix_probabilities.apply(lambda row: row.apply(lambda p: p * numpy.log(p + 1e-9)).sum(), axis=1) + destination_p_log_sum = self.transition_matrix_analyzer.transition_matrix_probabilities.apply(lambda row: row.apply(lambda p: p * numpy.log(p + 1e-9)).sum(), axis=1) - for aoi, s in destination_p_log_sum.items(): + for aoi, s in destination_p_log_sum.items(): - self.__transition_entropy += s * stationary_probabilities[aoi] + self.__transition_entropy += s * stationary_probabilities[aoi] - self.__transition_entropy *= -1 + self.__transition_entropy *= -1 - @property - def stationary_entropy(self) -> float: - """Stationary entropy.""" + @property + def stationary_entropy(self) -> float: + """Stationary entropy.""" - return self.__stationary_entropy + return self.__stationary_entropy - @property - def transition_entropy(self) -> float: - """Transition entropy.""" + @property + def transition_entropy(self) -> float: + """Transition entropy.""" - return self.__transition_entropy - \ No newline at end of file + return self.__transition_entropy + \ No newline at end of file diff --git a/src/argaze/GazeFeatures.py b/src/argaze/GazeFeatures.py index 5ef3c32..4aa65e7 100644 --- a/src/argaze/GazeFeatures.py +++ b/src/argaze/GazeFeatures.py @@ -86,10 +86,10 @@ class GazePosition(tuple): """Add position. !!! note - The returned position precision is the maximal precision. + The returned position precision is the maximal precision. !!! note - The returned position timestamp is the self object timestamp. + The returned position timestamp is the self object timestamp. """ if self.__precision is not None and position.precision is not None: @@ -106,10 +106,10 @@ class GazePosition(tuple): """Subtract position. !!! note - The returned position precision is the maximal precision. + The returned position precision is the maximal precision. !!! note - The returned position timestamp is the self object timestamp. + The returned position timestamp is the self object timestamp. """ if self.__precision is not None and position.precision is not None: @@ -124,10 +124,10 @@ class GazePosition(tuple): """Reversed subtract position. !!! note - The returned position precision is the maximal precision. + The returned position precision is the maximal precision. !!! note - The returned position timestamp is the self object timestamp. + The returned position timestamp is the self object timestamp. """ if self.__precision is not None and position.precision is not None: @@ -142,10 +142,10 @@ class GazePosition(tuple): """Multiply position by a factor. !!! note - The returned position precision is also multiplied by the factor. + The returned position precision is also multiplied by the factor. !!! note - The returned position timestamp is the self object timestamp. + The returned position timestamp is the self object timestamp. """ return GazePosition(tuple(numpy.array(self) * factor), precision=self.__precision * factor if self.__precision is not None else None, timestamp=self.timestamp) @@ -153,10 +153,10 @@ class GazePosition(tuple): """divide position by a factor. !!! note - The returned position precision is also divided by the factor. + The returned position precision is also divided by the factor. !!! note - The returned position timestamp is the self object timestamp. + The returned position timestamp is the self object timestamp. """ return GazePosition(tuple(numpy.array(self) / factor), precision=self.__precision / factor if self.__precision is not None else None, timestamp=self.timestamp) @@ -164,10 +164,10 @@ class GazePosition(tuple): """Power position by a factor. !!! note - The returned position precision is also powered by the factor. + The returned position precision is also powered by the factor. !!! note - The returned position timestamp is the self object timestamp. + The returned position timestamp is the self object timestamp. """ return GazePosition(tuple(numpy.array(self) ** factor), precision=self.__precision ** factor if self.__precision is not None else None, @@ -394,7 +394,7 @@ class GazeMovement(TimeStampedGazePositions): """Define abstract gaze movement class as timestamped gaze positions list. !!! note - Gaze movement timestamp is always equal to its first position timestamp. + Gaze movement timestamp is always equal to its first position timestamp. Parameters: positions: timestamp gaze positions. @@ -578,7 +578,7 @@ class GazeMovementIdentifier(DataFeatures.PipelineStepObject): """Identify gaze movement from successive timestamped gaze positions. !!! warning "Mandatory" - Each identified gaze movement have to share its first/last gaze position with previous/next gaze movement. + Each identified gaze movement have to share its first/last gaze position with previous/next gaze movement. Parameters: timestamped_gaze_position: new gaze position from where identification have to be done considering former gaze positions. @@ -694,7 +694,7 @@ class ScanStep(): last_saccade: a saccade that comes after the previous fixation. !!! warning - Scan step have to start by a fixation and then end by a saccade. + Scan step have to start by a fixation and then end by a saccade. """ def __init__(self, first_fixation: Fixation, last_saccade: Saccade): @@ -813,7 +813,7 @@ class ScanPath(list): def append_fixation(self, fixation): """Append new fixation to scan path. !!! warning - Consecutive fixations are ignored keeping the last fixation""" + Consecutive fixations are ignored keeping the last fixation""" self.__last_fixation = fixation @@ -925,7 +925,7 @@ class AOIScanStep(): letter: AOI unique letter to ease sequence analysis. !!! warning - Aoi scan step have to start by a fixation and then end by a saccade. + Aoi scan step have to start by a fixation and then end by a saccade. """ def __init__(self, movements: TimeStampedGazeMovements, aoi: str = '', letter: str = ''): @@ -1014,7 +1014,7 @@ class AOIScanPath(list): """Edit list of all expected aoi. !!! warning - This will clear the AOIScanPath + This will clear the AOIScanPath """ # Check expected aoi are not the same as previous ones @@ -1134,7 +1134,7 @@ class AOIScanPath(list): """Append new fixation to aoi scan path and return last new aoi scan step if one have been created. !!! warning - It could raise AOIScanStepError + It could raise AOIScanStepError """ # Replace None aoi by generic OutsideAOI name diff --git a/src/argaze/__init__.py b/src/argaze/__init__.py index a07fa93..be4cbfc 100644 --- a/src/argaze/__init__.py +++ b/src/argaze/__init__.py @@ -8,7 +8,7 @@ def load(filepath: str) -> any: Load object from json file. !!! note - The directory where json file is will be used as global working directory. + The directory where json file is will be used as global working directory. Parameters: filepath: path to json file diff --git a/src/argaze/utils/UtilsFeatures.py b/src/argaze/utils/UtilsFeatures.py index ff2bee6..23d6b24 100644 --- a/src/argaze/utils/UtilsFeatures.py +++ b/src/argaze/utils/UtilsFeatures.py @@ -250,7 +250,7 @@ class FileWriter(DataFeatures.PipelineStepObject): """Write data as a new line into file. !!! note - Tuple elements are converted into quoted strings separated by separator string. + Tuple elements are converted into quoted strings separated by separator string. """ # Format list or tuple element into quoted strings @@ -307,7 +307,7 @@ class FileReader(DataFeatures.PipelineStepObject): """Read next data from file. !!! note - Quoted strings separated by separator string are converted into tuple elements. + Quoted strings separated by separator string are converted into tuple elements. """ try: diff --git a/src/argaze/utils/contexts/TobiiProGlasses2.py b/src/argaze/utils/contexts/TobiiProGlasses2.py index 80487f4..7f45f32 100644 --- a/src/argaze/utils/contexts/TobiiProGlasses2.py +++ b/src/argaze/utils/contexts/TobiiProGlasses2.py @@ -395,7 +395,7 @@ class LiveStream(ArFeatures.LiveProcessingContext): @property def configuration(self) -> dict: - """Patch system configuration dictionary.""" + """Edit system configuration dictionary.""" return self.__configuration @configuration.setter @@ -471,7 +471,7 @@ class LiveStream(ArFeatures.LiveProcessingContext): """Bind to a participant or create one if it doesn't exist. !!! warning - Bind to a project before. + Bind to a project before. """ if self.__participant_name is None: -- cgit v1.1