aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThéo de la Hogue2022-11-21 17:00:53 +0100
committerThéo de la Hogue2022-11-21 17:00:53 +0100
commit1760f5c7f73b03caccb3d5039713dac41a02a910 (patch)
treeda825c4396dde947fc2b7ce2bca2615352c328d1
parent7da2d04b3ab6496704139a1270834308795557a4 (diff)
downloadargaze-1760f5c7f73b03caccb3d5039713dac41a02a910.zip
argaze-1760f5c7f73b03caccb3d5039713dac41a02a910.tar.gz
argaze-1760f5c7f73b03caccb3d5039713dac41a02a910.tar.bz2
argaze-1760f5c7f73b03caccb3d5039713dac41a02a910.tar.xz
Unifying and cleaning AreaOfInterest classes.
-rw-r--r--src/argaze/AreaOfInterest/AOI2DScene.py27
-rw-r--r--src/argaze/AreaOfInterest/AOI3DScene.py43
-rw-r--r--src/argaze/AreaOfInterest/AOIFeatures.py172
3 files changed, 144 insertions, 98 deletions
diff --git a/src/argaze/AreaOfInterest/AOI2DScene.py b/src/argaze/AreaOfInterest/AOI2DScene.py
index 99bf8a9..f648803 100644
--- a/src/argaze/AreaOfInterest/AOI2DScene.py
+++ b/src/argaze/AreaOfInterest/AOI2DScene.py
@@ -12,12 +12,9 @@ import numpy
class AOI2DScene(AOIFeatures.AOIScene):
"""Define AOI 2D scene."""
- def __init__(self, **aois_2d):
+ def __init__(self, aois_2d = None):
- super().__init__(**aois_2d)
-
- # set dimension member
- self.dimension = 2
+ super().__init__(2, aois_2d)
def draw(self, frame, exclude=[], color=(0, 255, 255)):
"""Draw AOI polygons on frame."""
@@ -29,7 +26,7 @@ class AOI2DScene(AOIFeatures.AOIScene):
aoi.draw(frame, color)
- def raycast(self, gaze_position) -> Tuple[str, "AOIFeatures.AreaOfInterest", bool]:
+ def raycast(self, gaze_position: GazeFeatures.GazePosition) -> Tuple[str, "AOIFeatures.AreaOfInterest", bool]:
"""Iterate over aoi to know which aoi is looked considering only gaze position value.
* **Returns:**
- aoi name
@@ -39,11 +36,11 @@ class AOI2DScene(AOIFeatures.AOIScene):
for name, aoi in self.items():
- looked = aoi.looked(gaze_position)
+ looked = aoi.contains_point(gaze_position.value)
yield name, aoi, looked
- def draw_raycast(self, frame, gaze_position, exclude=[], base_color=(0, 0, 255), looked_color=(0, 255, 0)):
+ def draw_raycast(self, frame, gaze_position: GazeFeatures.GazePosition, exclude=[], base_color=(0, 0, 255), looked_color=(0, 255, 0)):
"""Draw AOIs with their looked status."""
for name, aoi, looked in self.raycast(gaze_position):
@@ -61,26 +58,26 @@ class AOI2DScene(AOIFeatures.AOIScene):
# Draw form
aoi.draw(frame, color)
- def regioncast(self, gaze_position) -> Tuple[str, "AOIFeatures.AreaOfInterest", numpy.array, float, float]:
+ def circlecast(self, gaze_position: GazeFeatures.GazePosition) -> Tuple[str, "AOIFeatures.AreaOfInterest", numpy.array, float, float]:
"""Iterate over areas to know which aoi is looked considering gaze position value and its accuracy.
* **Returns:**
- aoi name
- aoi object
- looked region points
- - ratio of looked region relatively to aoi
- - ratio of looked region relatively to gaze position accuracy
+ - ratio of looked region area relatively to aoi area
+ - ratio of looked region area relatively to gaze position circle accuracy
"""
for name, aoi in self.items():
- looked_region, aoi_ratio, gaze_ratio = aoi.looked_region(gaze_position)
+ looked_region, aoi_ratio, gaze_ratio = aoi.circle_intersection(gaze_position.value, gaze_position.accuracy)
yield name, aoi, looked_region, aoi_ratio, gaze_ratio
- def draw_regioncast(self, frame, gaze_position, exclude=[], base_color=(0, 0, 255), looked_color=(0, 255, 0)):
- """Draw AOIs with their looked status and thei looked region."""
+ def draw_circlecast(self, frame, gaze_position: GazeFeatures.GazePosition, exclude=[], base_color=(0, 0, 255), looked_color=(0, 255, 0)):
+ """Draw AOIs with their looked status and looked region."""
- for name, aoi, looked_region, aoi_ratio, gaze_ratio in self.regioncast(gaze_position):
+ for name, aoi, looked_region, aoi_ratio, gaze_ratio in self.circlecast(gaze_position):
if name in exclude:
continue
diff --git a/src/argaze/AreaOfInterest/AOI3DScene.py b/src/argaze/AreaOfInterest/AOI3DScene.py
index ba0c18c..c5ee265 100644
--- a/src/argaze/AreaOfInterest/AOI3DScene.py
+++ b/src/argaze/AreaOfInterest/AOI3DScene.py
@@ -1,7 +1,6 @@
#!/usr/bin/env python
from typing import TypeVar, Tuple
-from dataclasses import dataclass, field
import math
import re
@@ -13,16 +12,16 @@ import cv2 as cv
T0 = numpy.array([0., 0., 0.])
-"""Define defaut translation vector."""
+"""Define no translation vector."""
R0 = numpy.array([0., 0., 0.])
-"""Define defaut rotation vector."""
+"""Define no rotation vector."""
-K0 = numpy.array([[1., 0., 1.], [0., 1., 1.], [0., 0., 1.]])
-"""Define defaut optical parameter."""
+K0 = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 0.]])
+"""Define default camera intrinsic parameters matrix."""
D0 = numpy.array([0.0, 0.0, 0.0, 0.0, 0.0])
-"""Define a zero distorsion matrix."""
+"""Define default camera distorsion coefficients vector."""
AOI3DSceneType = TypeVar('AOI3DScene', bound="AOI3DScene")
# Type definition for type annotation convenience
@@ -30,14 +29,12 @@ AOI3DSceneType = TypeVar('AOI3DScene', bound="AOI3DScene")
AOI2DSceneType = TypeVar('AOI2DScene', bound="AOI2DScene")
# Type definition for type annotation convenience
-@dataclass
class AOI3DScene(AOIFeatures.AOIScene):
"""Define AOI 3D scene."""
- def __post_init__(self, **aois):
+ def __init__(self, aois_3d = None):
- # set dimension member
- self.dimension = 3
+ super().__init__(3, aois_3d)
def load(self, obj_filepath: str):
"""Load AOI3D scene from .obj file."""
@@ -104,7 +101,7 @@ class AOI3DScene(AOIFeatures.AOIScene):
# retreive all aoi3D vertices
for name, face in faces.items():
- aoi3D = numpy.array([ vertices[i-1] for i in face ]).astype(numpy.float32).view(AOIFeatures.AreaOfInterest)
+ aoi3D = AOIFeatures.AreaOfInterest([ vertices[i-1] for i in face ])
self[name] = aoi3D
except IOError:
@@ -181,23 +178,35 @@ class AOI3DScene(AOIFeatures.AOIScene):
return aoi3D_scene_inside, aoi3D_scene_outside
- def project(self, T=T0, R=R0, K=K0, D=D0) -> AOI2DScene:
- """Project 3D scene onto 2D scene according translation, rotation and optical parameters."""
+ def project(self, T: numpy.array = T0, R: numpy.array = R0, K: numpy.array = K0, D: numpy.array = D0) -> AOI2DSceneType:
+ """Project 3D scene onto 2D scene according translation, rotation and optical parameters.
+
+ * **Arguments:**
+ - translation vector
+ - [axis-angle](https://en.wikipedia.org/wiki/Axis%E2%80%93angle_representation) rotation vector
+ - camera intrinsic parameters matrix
+ - camera distorsion coefficients vector
+ """
aoi2D_scene = AOI2DScene.AOI2DScene()
for name, aoi3D in self.items():
- vertices_2D, J = cv.projectPoints(aoi3D, R, T, K, D)
+ vertices_2D, J = cv.projectPoints(aoi3D.astype(numpy.float32), R, T, K, D)
- aoi2D = vertices_2D.reshape((len(vertices_2D), 2)).astype(numpy.float32).view(AOIFeatures.AreaOfInterest)
+ aoi2D = vertices_2D.reshape((len(vertices_2D), 2)).view(AOIFeatures.AreaOfInterest)
aoi2D_scene[name] = aoi2D
return aoi2D_scene
- def transform(self, T=T0, R=D0) -> AOI3DSceneType:
- """Translate and/or rotate 3D scene."""
+ def transform(self, T: numpy.array = T0, R: numpy.array = R0) -> AOI3DSceneType:
+ """Translate and/or rotate 3D scene.
+
+ * **Arguments:**
+ - translation vector
+ - [axis-angle](https://en.wikipedia.org/wiki/Axis%E2%80%93angle_representation) rotation vector
+ """
aoi3D_scene = AOI3DScene()
diff --git a/src/argaze/AreaOfInterest/AOIFeatures.py b/src/argaze/AreaOfInterest/AOIFeatures.py
index b3812bc..24e19e1 100644
--- a/src/argaze/AreaOfInterest/AOIFeatures.py
+++ b/src/argaze/AreaOfInterest/AOIFeatures.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python
-from dataclasses import dataclass, field
from typing import TypeVar, Tuple
+import json
from argaze import DataStructures
@@ -14,9 +14,18 @@ from shapely.geometry.point import Point
AreaOfInterestType = TypeVar('AreaOfInterest', bound="AreaOfInterest")
# Type definition for type annotation convenience
-@dataclass
class AreaOfInterest(numpy.ndarray):
- """Define 2D/3D Area Of Interest as an array of points."""
+ """Define Area Of Interest as an array of points of any dimension."""
+
+ def __new__(cls, points: numpy.ndarray) -> AreaOfInterestType:
+ """View casting inheritance."""
+
+ return numpy.array(points).view(AreaOfInterest)
+
+ def __repr__(self):
+ """String representation"""
+
+ return repr(self.tolist())
@property
def dimension(self) -> int:
@@ -25,13 +34,10 @@ class AreaOfInterest(numpy.ndarray):
return self.shape[1]
@property
- def bounding_box(self) -> numpy.array:
- """Get area's bounding box."""
-
- min_x, min_y = numpy.min(self, axis=0)
- max_x, max_y = numpy.max(self, axis=0)
-
- return numpy.array([(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)])
+ def size(self) -> int:
+ """Number of points defining the area."""
+
+ return self.shape[0]
@property
def center(self) -> numpy.array:
@@ -39,8 +45,22 @@ class AreaOfInterest(numpy.ndarray):
return self.mean(axis=0)
+ @property
+ def bounding_box(self) -> numpy.array:
+ """Get area's bounding box.
+ .. warning::
+ Available for 2D AOI only."""
+
+ assert(self.size > 1)
+ assert(self.dimension == 2)
+
+ min_x, min_y = numpy.min(self, axis=0)
+ max_x, max_y = numpy.max(self, axis=0)
+
+ return numpy.array([(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)])
+
def clockwise(self) -> AreaOfInterestType:
- """Get area points in clocwise order.
+ """Get area points in clockwise order.
.. warning::
Available for 2D AOI only."""
@@ -52,74 +72,81 @@ class AreaOfInterest(numpy.ndarray):
return self[numpy.argsort(angles)]
- def looked(self, gaze_position) -> bool:
- """Is gaze position inside area?
+ 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."""
assert(self.dimension == 2)
+ assert(len(point) == self.dimension)
- return mpath.Path(self).contains_points([tuple(gaze_position)])[0]
+ return mpath.Path(self).contains_points([point])[0]
- def look_at(self, pixel_position) -> numpy.array:
- """Get where the area is looked using perpespective transformation.
+ def inner_axis(self, point: tuple) -> tuple:
+ """Transform the coordinates from the global axis to the AOI's axis.
.. warning::
- Available for 2D AOI only."""
+ Available for 2D AOI only.
+ .. danger::
+ The AOI points must be sorted in clockwise order."""
assert(self.dimension == 2)
- Src = self.clockwise()
+ Src = self
Src_origin = Src[0]
- Src = (Src - Src_origin).reshape((len(Src)), 2)
+ Src = (Src - Src_origin).reshape((len(Src)), 2).astype(numpy.float32)
Dst = numpy.array([[0., 0.], [1., 0.], [1., 1.], [0., 1.]]).astype(numpy.float32)
P = cv.getPerspectiveTransform(Src, Dst)
- X = numpy.append(numpy.array(pixel_position - Src_origin), [1.0]).astype(numpy.float32)
+ X = numpy.append(numpy.array(numpy.array(point) - Src_origin), [1.0]).astype(numpy.float32)
Y = numpy.dot(P, X)
La = (Y/Y[2])[:-1]
- return numpy.around(La, 4).tolist()
+ return tuple(numpy.around(La, 4))
- def looked_pixel(self, look_at) -> numpy.array:
- """Get which pixel is looked inside 2D AOI.
+ def outter_axis(self, point: tuple) -> tuple:
+ """Transform the coordinates from the AOI's axis to the global axis.
.. warning::
- Available for 2D AOI only."""
+ Available for 2D AOI only.
+ .. danger::
+ The AOI points must be sorted in clockwise order."""
assert(self.dimension == 2)
Src = numpy.array([[0., 0.], [1., 0.], [1., 1.], [0., 1.]]).astype(numpy.float32)
- Dst = self.clockwise()
+ Dst = self.astype(numpy.float32)
Dst_origin = Dst[0]
Dst = (Dst - Dst_origin).reshape((len(Dst)), 2)
P = cv.getPerspectiveTransform(Src, Dst)
- X = numpy.array([look_at[0], look_at[1], 1.0]).astype(numpy.float32)
+ X = numpy.array([point[0], point[1], 1.0]).astype(numpy.float32)
Y = numpy.dot(P, X)
Lp = Dst_origin + (Y/Y[2])[:-1]
- return numpy.rint(Lp).astype(int).tolist()
+ return tuple(numpy.rint(Lp).astype(int))
- def looked_region(self, gaze_position) -> Tuple[numpy.array, float, float]:
- """Get intersection shape with gaze accuracy circle as the looked area, (looked area / AOI area) and (looked area / gaze accuracy circle area).
+ def circle_intersection(self, center: tuple, radius: float) -> Tuple[numpy.array, float, float]:
+ """Get intersection shape with a circle, intersection area / AOI area ration and intersection area / circle area ration.
.. warning::
Available for 2D AOI only."""
assert(self.dimension == 2)
self_polygon = Polygon(self)
- gaze_circle = Point(gaze_position).buffer(gaze_position.accuracy)
+ args_circle = Point(center).buffer(radius)
- if self_polygon.intersects(gaze_circle):
+ if self_polygon.intersects(args_circle):
- intersection = self_polygon.intersection(gaze_circle)
+ intersection = self_polygon.intersection(args_circle)
intersection_array = numpy.array([list(xy) for xy in intersection.exterior.coords[:]]).astype(numpy.float32).view(AreaOfInterest)
- return intersection_array, intersection.area / self_polygon.area, intersection.area / gaze_circle.area
+ return intersection_array, intersection.area / self_polygon.area, intersection.area / args_circle.area
else:
@@ -149,41 +176,62 @@ class AreaOfInterest(numpy.ndarray):
AOISceneType = TypeVar('AOIScene', bound="AOIScene")
# Type definition for type annotation convenience
-@dataclass
class AOIScene():
- """Define 2D/3D AOI scene."""
+ """Define AOI scene as a dictionary of AOI."""
- dimension: int = field(init=False, repr=False, default=None)
- """Dimension of the AOIs in scene."""
+ def __init__(self, dimension: int, areas: dict = None):
+ """Initialisation."""
- areas: dict = field(init=False, default_factory=dict)
- """All aois in the scene."""
+ assert(dimension > 0)
+
+ self.__dimension = dimension
+
+ # NEVER USE {} as default function argument
+ if areas == None:
+ self.__areas = {}
+ else:
+ self.__areas = areas
def __getitem__(self, name) -> AreaOfInterest:
- """Get an aoi from the scene."""
+ """Get an AOI from the scene."""
- return numpy.array(self.areas[name]).astype(numpy.float32).view(AreaOfInterest)
+ return AreaOfInterest(self.__areas[name]) #.astype(numpy.float32).view(AreaOfInterest)
def __setitem__(self, name, aoi: AreaOfInterest):
- """Add an aoi to the scene."""
+ """Add an AOI to the scene."""
- self.areas[name] = aoi.tolist()
+ assert(aoi.dimension == self.__dimension)
+
+ self.__areas[name] = AreaOfInterest(aoi) #.tolist()
def __delitem__(self, key):
- """Remove an aoi from the scene."""
+ """Remove an AOI from the scene."""
+
+ del self.__areas[key]
- del self.areas[key]
+ def __repr__(self):
+ """String representation"""
+
+ return str(self.__areas)
def items(self) -> Tuple[str, AreaOfInterest]:
"""Iterate over areas."""
- for name, area in self.areas.items():
- yield name, numpy.array(area).astype(numpy.float32).view(AreaOfInterest)
+ return self.__areas.items()
+
+ #for name, area in self.__areas.items():
+ #yield name, AreaOfInterest(area) #.astype(numpy.float32).view(AreaOfInterest)
def keys(self) -> list[str]:
"""Get areas name."""
- return self.areas.keys()
+ return self.__areas.keys()
+
+ @property
+ def dimension(self) -> int:
+ """Dimension of the AOIs in scene."""
+
+ return self.__dimension
@property
def bounds(self) -> numpy.array:
@@ -191,11 +239,11 @@ class AOIScene():
all_vertices = []
- for area in self.areas.values():
+ for area in self.__areas.values():
for vertice in area:
all_vertices.append(vertice)
- all_vertices = numpy.array(all_vertices).astype(numpy.float32)
+ all_vertices = numpy.array(all_vertices) #.astype(numpy.float32)
min_bounds = numpy.min(all_vertices, axis=0)
max_bounds = numpy.max(all_vertices, axis=0)
@@ -206,7 +254,7 @@ class AOIScene():
def center(self) -> numpy.array:
"""Get scene's center point."""
- min_bounds, max_bounds = self.bounds()
+ min_bounds, max_bounds = self.bounds
return (min_bounds + max_bounds) / 2
@@ -214,38 +262,30 @@ class AOIScene():
def size(self) -> numpy.array:
"""Get scene size."""
- min_bounds, max_bounds = self.bounds()
+ min_bounds, max_bounds = self.bounds
return max_bounds - min_bounds
def copy(self, exclude=[]) -> AOISceneType:
- """Copy scene partly excluding aoi by name."""
+ """Copy scene partly excluding AOI by name."""
- scene_copy = type(self)()
+ scene_copy = AOIScene(self.__dimension)
- for name, area in self.areas.items():
+ for name, area in self.__areas.items():
if name not in exclude:
- scene_copy[name] = numpy.array(area).astype(numpy.float32).view(AreaOfInterest)
+ scene_copy[name] = AreaOfInterest(area) #.astype(numpy.float32).view(AreaOfInterest)
return scene_copy
-class EmptyAOIScene(AOIScene):
- """Empty aoi scene."""
-
- def __init__(self):
-
- self.dimension = 0
- self.areas = {}
-
class TimeStampedAOIScenes(DataStructures.TimeStampedBuffer):
"""Define timestamped buffer to store AOI scenes in time."""
def __setitem__(self, ts, scene):
"""Force value to inherit from AOIScene."""
- assert(type(scene).__bases__[0] == AOIScene)
+ assert(type(scene) == AOIScene) # .__bases__[0]
super().__setitem__(ts, scene)