From 77f906729e46336b8e860b311cafdcf42087007c Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Tue, 8 Aug 2023 17:14:49 +0200 Subject: Renaming Pupil by Pupill. --- src/argaze.test/PupilAnalysis/WorkloadIndex.py | 56 -------- src/argaze.test/PupilAnalysis/__init__.py | 0 src/argaze.test/PupilFeatures.py | 167 ------------------------ src/argaze.test/PupillAnalysis/WorkloadIndex.py | 56 ++++++++ src/argaze.test/PupillAnalysis/__init__.py | 0 src/argaze.test/PupillFeatures.py | 167 ++++++++++++++++++++++++ src/argaze/PupilAnalysis/WorkloadIndex.py | 64 --------- src/argaze/PupilAnalysis/__init__.py | 4 - src/argaze/PupilFeatures.py | 106 --------------- src/argaze/PupillAnalysis/WorkloadIndex.py | 64 +++++++++ src/argaze/PupillAnalysis/__init__.py | 4 + src/argaze/PupillFeatures.py | 106 +++++++++++++++ src/argaze/__init__.py | 2 +- 13 files changed, 398 insertions(+), 398 deletions(-) delete mode 100644 src/argaze.test/PupilAnalysis/WorkloadIndex.py delete mode 100644 src/argaze.test/PupilAnalysis/__init__.py delete mode 100644 src/argaze.test/PupilFeatures.py create mode 100644 src/argaze.test/PupillAnalysis/WorkloadIndex.py create mode 100644 src/argaze.test/PupillAnalysis/__init__.py create mode 100644 src/argaze.test/PupillFeatures.py delete mode 100644 src/argaze/PupilAnalysis/WorkloadIndex.py delete mode 100644 src/argaze/PupilAnalysis/__init__.py delete mode 100644 src/argaze/PupilFeatures.py create mode 100644 src/argaze/PupillAnalysis/WorkloadIndex.py create mode 100644 src/argaze/PupillAnalysis/__init__.py create mode 100644 src/argaze/PupillFeatures.py diff --git a/src/argaze.test/PupilAnalysis/WorkloadIndex.py b/src/argaze.test/PupilAnalysis/WorkloadIndex.py deleted file mode 100644 index fec32ef..0000000 --- a/src/argaze.test/PupilAnalysis/WorkloadIndex.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python - -""" """ - -__author__ = "Théo de la Hogue" -__credits__ = [] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "BSD" - -import unittest -import math - -from argaze import PupilFeatures -from argaze.PupilAnalysis import WorkloadIndex - -class TestWorkloadIndexClass(unittest.TestCase): - """Test WorkloadIndex class.""" - - def test_analysis(self): - """Test WorkloadIndex analysis.""" - - ts_pupil_diameters = { - 0: PupilFeatures.PupilDiameter(1.), - 1: PupilFeatures.PupilDiameter(1.1), - 2: PupilFeatures.PupilDiameter(1.2), - 3: PupilFeatures.PupilDiameter(1.3), - 4: PupilFeatures.PupilDiameter(1.2), - 5: PupilFeatures.PupilDiameter(1.1), - 6: PupilFeatures.PupilDiameter(1.), - 7: PupilFeatures.PupilDiameter(0.9), - 8: PupilFeatures.PupilDiameter(0.8), - 9: PupilFeatures.PupilDiameter(0.7) - } - - pupil_diameter_analyzer = WorkloadIndex.PupilDiameterAnalyzer(reference=PupilFeatures.PupilDiameter(1.), period=3) - ts_analysis = pupil_diameter_analyzer.browse(PupilFeatures.TimeStampedPupilDiameters(ts_pupil_diameters)) - - # Check result size - self.assertEqual(len(ts_analysis), 3) - - # Check each workload index - ts_1, analysis_1 = ts_analysis.pop_first() - self.assertEqual(ts_1, 3) - self.assertTrue(math.isclose(analysis_1, 0.1, abs_tol=1e-2)) - - ts_2, analysis_2 = ts_analysis.pop_first() - self.assertEqual(ts_2, 6) - self.assertTrue(math.isclose(analysis_2, 0.2, abs_tol=1e-2)) - - ts_3, analysis_3 = ts_analysis.pop_first() - self.assertEqual(ts_3, 9) - self.assertTrue(math.isclose(analysis_3, -0.1, abs_tol=1e-2)) - -if __name__ == '__main__': - - unittest.main() \ No newline at end of file diff --git a/src/argaze.test/PupilAnalysis/__init__.py b/src/argaze.test/PupilAnalysis/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/argaze.test/PupilFeatures.py b/src/argaze.test/PupilFeatures.py deleted file mode 100644 index ecc94cd..0000000 --- a/src/argaze.test/PupilFeatures.py +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env python - -""" """ - -__author__ = "Théo de la Hogue" -__credits__ = [] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "BSD" - -import unittest - -from argaze import PupilFeatures - -import numpy - -def random_pupil_diameters(size): - """ Generate random TimeStampedPupilDiameters for testing purpose. - Timestamps are current time. - PupilDiameters are random values. - """ - - import random - import time - - ts_pupil_diameters = PupilFeatures.TimeStampedPupilDiameters() - - for i in range(0, size): - - # Edit pupil diameter - random_pupil_diameter = PupilFeatures.PupilDiameter(random.random()) - - # Store pupil diameter - ts_pupil_diameters[time.time()] = random_pupil_diameter - - return ts_pupil_diameters - -class TestPupilDiameterClass(unittest.TestCase): - """Test PupilDiameter class.""" - - def test_new(self): - """Test PupilDiameter creation.""" - - # Check empty PupilDiameter - empty_pupil_diameter = PupilFeatures.PupilDiameter() - - self.assertEqual(empty_pupil_diameter.value, 0.) - self.assertEqual(empty_pupil_diameter.valid, False) - - # Check float PupilDiameter - float_pupil_diameter = PupilFeatures.PupilDiameter(1.23) - - self.assertEqual(float_pupil_diameter.value, 1.23) - self.assertEqual(float_pupil_diameter.valid, True) - - def test_properties(self): - """Test PupilDiameter properties cannot be modified after creation.""" - - pupil_diameter = PupilFeatures.PupilDiameter() - - # Check that pupil diameter value setting fails - with self.assertRaises(AttributeError): - - pupil_diameter.value = 123 - - self.assertNotEqual(pupil_diameter.value, 123) - self.assertEqual(pupil_diameter.value, 0.) - - def test___repr__(self): - """Test PupilDiameter string representation.""" - - # Check empty PupilDiameter representation - self.assertEqual(repr(PupilFeatures.PupilDiameter()), "{\"value\": 0.0}") - -class TestUnvalidPupilDiameterClass(unittest.TestCase): - """Test UnvalidPupilDiameter class.""" - - def test_new(self): - """Test UnvalidPupilDiameter creation.""" - - unvalid_pupil_diameter = PupilFeatures.UnvalidPupilDiameter() - - self.assertEqual(unvalid_pupil_diameter.value, 0.) - self.assertEqual(unvalid_pupil_diameter.valid, False) - - def test___repr__(self): - """Test UnvalidPupilDiameter string representation.""" - - self.assertEqual(repr(PupilFeatures.UnvalidPupilDiameter()), "{\"message\": null, \"value\": 0.0}") - -class TestTimeStampedPupilDiametersClass(unittest.TestCase): - """Test TimeStampedPupilDiameters class.""" - - def test___setitem__(self): - """Test __setitem__ method.""" - - ts_pupil_diameters = PupilFeatures.TimeStampedPupilDiameters() - ts_pupil_diameters[0] = PupilFeatures.PupilDiameter() - ts_pupil_diameters[1] = PupilFeatures.UnvalidPupilDiameter() - ts_pupil_diameters[2] = {"value": 1.23} - - # Check PupilDiameter is correctly stored and accessible as a PupilDiameter - self.assertIsInstance(ts_pupil_diameters[0], PupilFeatures.PupilDiameter) - self.assertEqual(ts_pupil_diameters[0].valid, False) - - # Check UnvalidPupilDiameter is correctly stored and accessible as a UnvalidPupilDiameter - self.assertIsInstance(ts_pupil_diameters[1], PupilFeatures.UnvalidPupilDiameter) - self.assertEqual(ts_pupil_diameters[1].valid, False) - - # Check dict with "value" and "precision" keys is correctly stored and accessible as a PupilDiameter - self.assertIsInstance(ts_pupil_diameters[2], PupilFeatures.PupilDiameter) - self.assertEqual(ts_pupil_diameters[2].valid, True) - - # Check that bad data type insertion fails - with self.assertRaises(AssertionError): - - ts_pupil_diameters[3] = "This string is not a pupil diameter value." - - # Check that dict with bad keys insertion fails - with self.assertRaises(AssertionError): - - ts_pupil_diameters[4] = {"bad_key": 0.} - - def test___repr__(self): - """Test inherited string representation.""" - - ts_pupil_diameters = PupilFeatures.TimeStampedPupilDiameters() - - self.assertEqual(repr(PupilFeatures.TimeStampedPupilDiameters()), "{}") - - ts_pupil_diameters[0] = PupilFeatures.PupilDiameter() - - self.assertEqual(repr(ts_pupil_diameters), "{\"0\": {\"value\": 0.0}}") - - ts_pupil_diameters[0] = PupilFeatures.UnvalidPupilDiameter() - - self.assertEqual(repr(ts_pupil_diameters), "{\"0\": {\"message\": null, \"value\": 0.0}}") - - def test_as_dataframe(self): - """Test inherited as_dataframe method.""" - - ts_pupil_diameters_dataframe = random_pupil_diameters(10).as_dataframe() - - # Check dataframe conversion - self.assertEqual(ts_pupil_diameters_dataframe.index.name, "timestamp") - self.assertEqual(ts_pupil_diameters_dataframe.index.size, 10) - - self.assertEqual(ts_pupil_diameters_dataframe.columns.size, 1) - self.assertEqual(ts_pupil_diameters_dataframe.columns[0], "value") - - self.assertEqual(ts_pupil_diameters_dataframe["value"].dtype, 'float64') - - # Check unvalid diameter conversion - ts_pupil_diameters = PupilFeatures.TimeStampedPupilDiameters() - ts_pupil_diameters[0] = PupilFeatures.UnvalidPupilDiameter() - ts_pupil_diameters_dataframe = ts_pupil_diameters.as_dataframe() - - self.assertEqual(ts_pupil_diameters_dataframe.index.name, "timestamp") - self.assertEqual(ts_pupil_diameters_dataframe.index.size, 1) - - self.assertEqual(ts_pupil_diameters_dataframe.columns.size, 1) - self.assertEqual(ts_pupil_diameters_dataframe.columns[0], "value") - - self.assertEqual(ts_pupil_diameters_dataframe["value"].dtype, 'float64') - -if __name__ == '__main__': - - unittest.main() \ No newline at end of file diff --git a/src/argaze.test/PupillAnalysis/WorkloadIndex.py b/src/argaze.test/PupillAnalysis/WorkloadIndex.py new file mode 100644 index 0000000..d60b6ae --- /dev/null +++ b/src/argaze.test/PupillAnalysis/WorkloadIndex.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python + +""" """ + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "BSD" + +import unittest +import math + +from argaze import PupillFeatures +from argaze.PupillAnalysis import WorkloadIndex + +class TestWorkloadIndexClass(unittest.TestCase): + """Test WorkloadIndex class.""" + + def test_analysis(self): + """Test WorkloadIndex analysis.""" + + ts_pupill_diameters = { + 0: PupillFeatures.PupillDiameter(1.), + 1: PupillFeatures.PupillDiameter(1.1), + 2: PupillFeatures.PupillDiameter(1.2), + 3: PupillFeatures.PupillDiameter(1.3), + 4: PupillFeatures.PupillDiameter(1.2), + 5: PupillFeatures.PupillDiameter(1.1), + 6: PupillFeatures.PupillDiameter(1.), + 7: PupillFeatures.PupillDiameter(0.9), + 8: PupillFeatures.PupillDiameter(0.8), + 9: PupillFeatures.PupillDiameter(0.7) + } + + pupill_diameter_analyzer = WorkloadIndex.PupillDiameterAnalyzer(reference=PupillFeatures.PupillDiameter(1.), period=3) + ts_analysis = pupill_diameter_analyzer.browse(PupillFeatures.TimeStampedPupillDiameters(ts_pupill_diameters)) + + # Check result size + self.assertEqual(len(ts_analysis), 3) + + # Check each workload index + ts_1, analysis_1 = ts_analysis.pop_first() + self.assertEqual(ts_1, 3) + self.assertTrue(math.isclose(analysis_1, 0.1, abs_tol=1e-2)) + + ts_2, analysis_2 = ts_analysis.pop_first() + self.assertEqual(ts_2, 6) + self.assertTrue(math.isclose(analysis_2, 0.2, abs_tol=1e-2)) + + ts_3, analysis_3 = ts_analysis.pop_first() + self.assertEqual(ts_3, 9) + self.assertTrue(math.isclose(analysis_3, -0.1, abs_tol=1e-2)) + +if __name__ == '__main__': + + unittest.main() \ No newline at end of file diff --git a/src/argaze.test/PupillAnalysis/__init__.py b/src/argaze.test/PupillAnalysis/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/argaze.test/PupillFeatures.py b/src/argaze.test/PupillFeatures.py new file mode 100644 index 0000000..f0e8e1b --- /dev/null +++ b/src/argaze.test/PupillFeatures.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python + +""" """ + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "BSD" + +import unittest + +from argaze import PupillFeatures + +import numpy + +def random_pupill_diameters(size): + """ Generate random TimeStampedPupillDiameters for testing purpose. + Timestamps are current time. + PupillDiameters are random values. + """ + + import random + import time + + ts_pupill_diameters = PupillFeatures.TimeStampedPupillDiameters() + + for i in range(0, size): + + # Edit pupill diameter + random_pupill_diameter = PupillFeatures.PupillDiameter(random.random()) + + # Store pupill diameter + ts_pupill_diameters[time.time()] = random_pupill_diameter + + return ts_pupill_diameters + +class TestPupillDiameterClass(unittest.TestCase): + """Test PupillDiameter class.""" + + def test_new(self): + """Test PupillDiameter creation.""" + + # Check empty PupillDiameter + empty_pupill_diameter = PupillFeatures.PupillDiameter() + + self.assertEqual(empty_pupill_diameter.value, 0.) + self.assertEqual(empty_pupill_diameter.valid, False) + + # Check float PupillDiameter + float_pupill_diameter = PupillFeatures.PupillDiameter(1.23) + + self.assertEqual(float_pupill_diameter.value, 1.23) + self.assertEqual(float_pupill_diameter.valid, True) + + def test_properties(self): + """Test PupillDiameter properties cannot be modified after creation.""" + + pupill_diameter = PupillFeatures.PupillDiameter() + + # Check that pupill diameter value setting fails + with self.assertRaises(AttributeError): + + pupill_diameter.value = 123 + + self.assertNotEqual(pupill_diameter.value, 123) + self.assertEqual(pupill_diameter.value, 0.) + + def test___repr__(self): + """Test PupillDiameter string representation.""" + + # Check empty PupillDiameter representation + self.assertEqual(repr(PupillFeatures.PupillDiameter()), "{\"value\": 0.0}") + +class TestUnvalidPupillDiameterClass(unittest.TestCase): + """Test UnvalidPupillDiameter class.""" + + def test_new(self): + """Test UnvalidPupillDiameter creation.""" + + unvalid_pupill_diameter = PupillFeatures.UnvalidPupillDiameter() + + self.assertEqual(unvalid_pupill_diameter.value, 0.) + self.assertEqual(unvalid_pupill_diameter.valid, False) + + def test___repr__(self): + """Test UnvalidPupillDiameter string representation.""" + + self.assertEqual(repr(PupillFeatures.UnvalidPupillDiameter()), "{\"message\": null, \"value\": 0.0}") + +class TestTimeStampedPupillDiametersClass(unittest.TestCase): + """Test TimeStampedPupillDiameters class.""" + + def test___setitem__(self): + """Test __setitem__ method.""" + + ts_pupill_diameters = PupillFeatures.TimeStampedPupillDiameters() + ts_pupill_diameters[0] = PupillFeatures.PupillDiameter() + ts_pupill_diameters[1] = PupillFeatures.UnvalidPupillDiameter() + ts_pupill_diameters[2] = {"value": 1.23} + + # Check PupillDiameter is correctly stored and accessible as a PupillDiameter + self.assertIsInstance(ts_pupill_diameters[0], PupillFeatures.PupillDiameter) + self.assertEqual(ts_pupill_diameters[0].valid, False) + + # Check UnvalidPupillDiameter is correctly stored and accessible as a UnvalidPupillDiameter + self.assertIsInstance(ts_pupill_diameters[1], PupillFeatures.UnvalidPupillDiameter) + self.assertEqual(ts_pupill_diameters[1].valid, False) + + # Check dict with "value" and "precision" keys is correctly stored and accessible as a PupillDiameter + self.assertIsInstance(ts_pupill_diameters[2], PupillFeatures.PupillDiameter) + self.assertEqual(ts_pupill_diameters[2].valid, True) + + # Check that bad data type insertion fails + with self.assertRaises(AssertionError): + + ts_pupill_diameters[3] = "This string is not a pupill diameter value." + + # Check that dict with bad keys insertion fails + with self.assertRaises(AssertionError): + + ts_pupill_diameters[4] = {"bad_key": 0.} + + def test___repr__(self): + """Test inherited string representation.""" + + ts_pupill_diameters = PupillFeatures.TimeStampedPupillDiameters() + + self.assertEqual(repr(PupillFeatures.TimeStampedPupillDiameters()), "{}") + + ts_pupill_diameters[0] = PupillFeatures.PupillDiameter() + + self.assertEqual(repr(ts_pupill_diameters), "{\"0\": {\"value\": 0.0}}") + + ts_pupill_diameters[0] = PupillFeatures.UnvalidPupillDiameter() + + self.assertEqual(repr(ts_pupill_diameters), "{\"0\": {\"message\": null, \"value\": 0.0}}") + + def test_as_dataframe(self): + """Test inherited as_dataframe method.""" + + ts_pupill_diameters_dataframe = random_pupill_diameters(10).as_dataframe() + + # Check dataframe conversion + self.assertEqual(ts_pupill_diameters_dataframe.index.name, "timestamp") + self.assertEqual(ts_pupill_diameters_dataframe.index.size, 10) + + self.assertEqual(ts_pupill_diameters_dataframe.columns.size, 1) + self.assertEqual(ts_pupill_diameters_dataframe.columns[0], "value") + + self.assertEqual(ts_pupill_diameters_dataframe["value"].dtype, 'float64') + + # Check unvalid diameter conversion + ts_pupill_diameters = PupillFeatures.TimeStampedPupillDiameters() + ts_pupill_diameters[0] = PupillFeatures.UnvalidPupillDiameter() + ts_pupill_diameters_dataframe = ts_pupill_diameters.as_dataframe() + + self.assertEqual(ts_pupill_diameters_dataframe.index.name, "timestamp") + self.assertEqual(ts_pupill_diameters_dataframe.index.size, 1) + + self.assertEqual(ts_pupill_diameters_dataframe.columns.size, 1) + self.assertEqual(ts_pupill_diameters_dataframe.columns[0], "value") + + self.assertEqual(ts_pupill_diameters_dataframe["value"].dtype, 'float64') + +if __name__ == '__main__': + + unittest.main() \ No newline at end of file diff --git a/src/argaze/PupilAnalysis/WorkloadIndex.py b/src/argaze/PupilAnalysis/WorkloadIndex.py deleted file mode 100644 index f86a9e8..0000000 --- a/src/argaze/PupilAnalysis/WorkloadIndex.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python - -""" """ - -__author__ = "Théo de la Hogue" -__credits__ = ["Jean-Paul Imbert"] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "BSD" - -from typing import TypeVar -from dataclasses import dataclass, field -import math - -from argaze import PupilFeatures - -import numpy - -@dataclass -class PupilDiameterAnalyzer(PupilFeatures.PupilDiameterAnalyzer): - """Periodic average of pupil diameter variations to pupil diameter reference value.""" - - reference: PupilFeatures.PupilDiameter - """ """ - - period: int | float = field(default=1) - """Identification period length.""" - - def __post_init__(self): - - assert(self.reference.valid) - - self.__variations_sum = 0. - self.__variations_number = 0 - self.__last_ts = 0 - - def analyze(self, ts, pupil_diameter) -> float: - """Analyze workload index from successive timestamped pupil diameters.""" - - # Ignore non valid pupil diameter - if not pupil_diameter.valid: - - return None - - if ts - self.__last_ts >= self.period: - - if self.__variations_number > 0 and self.reference.value > 0.: - - workload_index = (self.__variations_sum / self.__variations_number) / self.reference.value - - else: - - workload_index = 0. - - self.__variations_sum = pupil_diameter.value - self.reference.value - self.__variations_number = 1 - self.__last_ts = ts - - return workload_index - - else: - - self.__variations_sum += pupil_diameter.value - self.reference.value - self.__variations_number += 1 - \ No newline at end of file diff --git a/src/argaze/PupilAnalysis/__init__.py b/src/argaze/PupilAnalysis/__init__.py deleted file mode 100644 index c563968..0000000 --- a/src/argaze/PupilAnalysis/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -""" -Class interface to work with various pupil analysis algorithms. -""" -__all__ = ['WorkloadIndex'] \ No newline at end of file diff --git a/src/argaze/PupilFeatures.py b/src/argaze/PupilFeatures.py deleted file mode 100644 index 3ba9576..0000000 --- a/src/argaze/PupilFeatures.py +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env python - -""" """ - -__author__ = "Théo de la Hogue" -__credits__ = [] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "BSD" - -from typing import TypeVar -from dataclasses import dataclass, field -import json - -from argaze import DataStructures - -@dataclass(frozen=True) -class PupilDiameter(): - """Define pupil diameter as ...""" - - value: float = field(default=0.) - """Pupil diameter value.""" - - @property - def valid(self) -> bool: - """Is the value not 0""" - - return self.value != 0. - - def __repr__(self): - """String representation""" - - return json.dumps(self, ensure_ascii = False, default=vars) - -class UnvalidPupilDiameter(PupilDiameter): - """Unvalid pupil diameter.""" - - def __init__(self, message=None): - - self.message = message - - super().__init__(0.) - -TimeStampedPupilDiametersType = TypeVar('TimeStampedPupilDiameters', bound="TimeStampedPupilDiameters") -# Type definition for type annotation convenience - -class TimeStampedPupilDiameters(DataStructures.TimeStampedBuffer): - """Define timestamped buffer to store pupil diameters.""" - - def __setitem__(self, key, value: PupilDiameter|dict): - """Force PupilDiameter storage.""" - - # Convert dict into PupilDiameter - if type(value) == dict: - - assert(set(['value']).issubset(value.keys())) - - if 'message' in value.keys(): - - value = UnvalidPupilDiameter(value['message']) - - else: - - value = PupilDiameter(value['value']) - - assert(type(value) == PupilDiameter or type(value) == UnvalidPupilDiameter) - - super().__setitem__(key, value) - - @classmethod - def from_json(self, json_filepath: str) -> TimeStampedPupilDiametersType: - """Create a TimeStampedPupilDiametersType from .json file.""" - - with open(json_filepath, encoding='utf-8') as ts_buffer_file: - - json_buffer = json.load(ts_buffer_file) - - return TimeStampedPupilDiameters({ast.literal_eval(ts_str): json_buffer[ts_str] for ts_str in json_buffer}) - -TimeStampedBufferType = TypeVar('TimeStampedBuffer', bound="TimeStampedBuffer") -# Type definition for type annotation convenience - -class PupilDiameterAnalyzer(): - """Abstract class to define what should provide a pupil diameter analyser.""" - - def analyze(self, ts, pupil_diameter) -> float: - """Analyze pupil diameter from successive timestamped pupil diameters.""" - - raise NotImplementedError('analyze() method not implemented') - - def browse(self, ts_pupil_diameters: TimeStampedPupilDiameters) -> TimeStampedBufferType: - """Analyze by browsing timestamped pupil diameters.""" - - assert(type(ts_pupil_diameters) == TimeStampedPupilDiameters) - - ts_analyzis = DataStructures.TimeStampedBuffer() - - # Iterate on pupil diameters - for ts, pupil_diameter in ts_pupil_diameters.items(): - - analysis = self.analyze(ts, pupil_diameter) - - if analysis is not None: - - ts_analyzis[ts] = analysis - - return ts_analyzis diff --git a/src/argaze/PupillAnalysis/WorkloadIndex.py b/src/argaze/PupillAnalysis/WorkloadIndex.py new file mode 100644 index 0000000..4a20091 --- /dev/null +++ b/src/argaze/PupillAnalysis/WorkloadIndex.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +""" """ + +__author__ = "Théo de la Hogue" +__credits__ = ["Jean-Paul Imbert"] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "BSD" + +from typing import TypeVar +from dataclasses import dataclass, field +import math + +from argaze import PupillFeatures + +import numpy + +@dataclass +class PupillDiameterAnalyzer(PupillFeatures.PupillDiameterAnalyzer): + """Periodic average of pupill diameter variations to pupill diameter reference value.""" + + reference: PupillFeatures.PupillDiameter + """ """ + + period: int | float = field(default=1) + """Identification period length.""" + + def __post_init__(self): + + assert(self.reference.valid) + + self.__variations_sum = 0. + self.__variations_number = 0 + self.__last_ts = 0 + + def analyze(self, ts, pupill_diameter) -> float: + """Analyze workload index from successive timestamped pupill diameters.""" + + # Ignore non valid pupill diameter + if not pupill_diameter.valid: + + return None + + if ts - self.__last_ts >= self.period: + + if self.__variations_number > 0 and self.reference.value > 0.: + + workload_index = (self.__variations_sum / self.__variations_number) / self.reference.value + + else: + + workload_index = 0. + + self.__variations_sum = pupill_diameter.value - self.reference.value + self.__variations_number = 1 + self.__last_ts = ts + + return workload_index + + else: + + self.__variations_sum += pupill_diameter.value - self.reference.value + self.__variations_number += 1 + \ No newline at end of file diff --git a/src/argaze/PupillAnalysis/__init__.py b/src/argaze/PupillAnalysis/__init__.py new file mode 100644 index 0000000..18f0f15 --- /dev/null +++ b/src/argaze/PupillAnalysis/__init__.py @@ -0,0 +1,4 @@ +""" +Class interface to work with various pupill analysis algorithms. +""" +__all__ = ['WorkloadIndex'] \ No newline at end of file diff --git a/src/argaze/PupillFeatures.py b/src/argaze/PupillFeatures.py new file mode 100644 index 0000000..05408a3 --- /dev/null +++ b/src/argaze/PupillFeatures.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python + +""" """ + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "BSD" + +from typing import TypeVar +from dataclasses import dataclass, field +import json + +from argaze import DataStructures + +@dataclass(frozen=True) +class PupillDiameter(): + """Define pupill diameter as ...""" + + value: float = field(default=0.) + """Pupill diameter value.""" + + @property + def valid(self) -> bool: + """Is the value not 0""" + + return self.value != 0. + + def __repr__(self): + """String representation""" + + return json.dumps(self, ensure_ascii = False, default=vars) + +class UnvalidPupillDiameter(PupillDiameter): + """Unvalid pupill diameter.""" + + def __init__(self, message=None): + + self.message = message + + super().__init__(0.) + +TimeStampedPupillDiametersType = TypeVar('TimeStampedPupillDiameters', bound="TimeStampedPupillDiameters") +# Type definition for type annotation convenience + +class TimeStampedPupillDiameters(DataStructures.TimeStampedBuffer): + """Define timestamped buffer to store pupill diameters.""" + + def __setitem__(self, key, value: PupillDiameter|dict): + """Force PupillDiameter storage.""" + + # Convert dict into PupillDiameter + if type(value) == dict: + + assert(set(['value']).issubset(value.keys())) + + if 'message' in value.keys(): + + value = UnvalidPupillDiameter(value['message']) + + else: + + value = PupillDiameter(value['value']) + + assert(type(value) == PupillDiameter or type(value) == UnvalidPupillDiameter) + + super().__setitem__(key, value) + + @classmethod + def from_json(self, json_filepath: str) -> TimeStampedPupillDiametersType: + """Create a TimeStampedPupillDiametersType from .json file.""" + + with open(json_filepath, encoding='utf-8') as ts_buffer_file: + + json_buffer = json.load(ts_buffer_file) + + return TimeStampedPupillDiameters({ast.literal_eval(ts_str): json_buffer[ts_str] for ts_str in json_buffer}) + +TimeStampedBufferType = TypeVar('TimeStampedBuffer', bound="TimeStampedBuffer") +# Type definition for type annotation convenience + +class PupillDiameterAnalyzer(): + """Abstract class to define what should provide a pupill diameter analyser.""" + + def analyze(self, ts, pupill_diameter) -> float: + """Analyze pupill diameter from successive timestamped pupill diameters.""" + + raise NotImplementedError('analyze() method not implemented') + + def browse(self, ts_pupill_diameters: TimeStampedPupillDiameters) -> TimeStampedBufferType: + """Analyze by browsing timestamped pupill diameters.""" + + assert(type(ts_pupill_diameters) == TimeStampedPupillDiameters) + + ts_analyzis = DataStructures.TimeStampedBuffer() + + # Iterate on pupill diameters + for ts, pupill_diameter in ts_pupill_diameters.items(): + + analysis = self.analyze(ts, pupill_diameter) + + if analysis is not None: + + ts_analyzis[ts] = analysis + + return ts_analyzis diff --git a/src/argaze/__init__.py b/src/argaze/__init__.py index 5b2685b..9f79a17 100644 --- a/src/argaze/__init__.py +++ b/src/argaze/__init__.py @@ -1,4 +1,4 @@ """ ArGaze is divided in submodules dedicated to various specifics features. """ -__all__ = ['ArUcoMarkers','AreaOfInterest','ArFeatures','GazeFeatures','GazeAnalysis','PupilFeatures','PupilAnalysis','DataStructures','utils'] \ No newline at end of file +__all__ = ['ArUcoMarkers','AreaOfInterest','ArFeatures','GazeFeatures','GazeAnalysis','PupillFeatures','PupillAnalysis','DataStructures','utils'] \ No newline at end of file -- cgit v1.1