aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTheo De La Hogue2023-09-25 14:46:46 +0200
committerTheo De La Hogue2023-09-25 14:46:46 +0200
commit217d7ffb68ea4ebbc22cd914cf37d24ce3bcc566 (patch)
treeffe07f909b76d721e8896bb8d5a1e30679a78192 /src
parent8f2b87bfec622dd32e90d9bfa17dfcda42add4fe (diff)
downloadargaze-217d7ffb68ea4ebbc22cd914cf37d24ce3bcc566.zip
argaze-217d7ffb68ea4ebbc22cd914cf37d24ce3bcc566.tar.gz
argaze-217d7ffb68ea4ebbc22cd914cf37d24ce3bcc566.tar.bz2
argaze-217d7ffb68ea4ebbc22cd914cf37d24ce3bcc566.tar.xz
Adding a way to load SVG AOI description. Allowing to use shape to describe rectangular or circular 2D AOI in JSON.
Diffstat (limited to 'src')
-rw-r--r--src/argaze/ArFeatures.py5
-rw-r--r--src/argaze/AreaOfInterest/AOI2DScene.py62
-rw-r--r--src/argaze/AreaOfInterest/AOIFeatures.py48
3 files changed, 112 insertions, 3 deletions
diff --git a/src/argaze/ArFeatures.py b/src/argaze/ArFeatures.py
index a419d93..0750cb5 100644
--- a/src/argaze/ArFeatures.py
+++ b/src/argaze/ArFeatures.py
@@ -180,6 +180,11 @@ class ArLayer():
new_aoi_scene = AOIFeatures.AOIScene.from_json(filepath)
+ # SVG file format for 2D dimension only
+ if file_format == 'svg':
+
+ new_aoi_scene = AOIFeatures.AOI2DScene.from_svg(filepath)
+
# OBJ file format for 3D dimension only
elif file_format == 'obj':
diff --git a/src/argaze/AreaOfInterest/AOI2DScene.py b/src/argaze/AreaOfInterest/AOI2DScene.py
index 564f65c..4dc47f4 100644
--- a/src/argaze/AreaOfInterest/AOI2DScene.py
+++ b/src/argaze/AreaOfInterest/AOI2DScene.py
@@ -15,6 +15,7 @@ from argaze import GazeFeatures
import cv2
import numpy
+from xml.dom import minidom
AOI2DSceneType = TypeVar('AOI2DScene', bound="AOI2DScene")
# Type definition for type annotation convenience
@@ -29,6 +30,67 @@ class AOI2DScene(AOIFeatures.AOIScene):
super().__init__(2, aois_2d)
+ @classmethod
+ def from_svg(self, svg_filepath: str) -> AOI2DSceneType:
+ """
+ Load areas from .svg file.
+
+ Parameters:
+ svg_filepath: path to svg file
+
+ !!! note
+ Available SVG elements are: path, rect and circle.
+
+ !!! warning
+ Available SVG path d-string commands are: MoveTo (M) LineTo (L) and ClosePath (Z) commands.
+ """
+
+ with minidom.parse(svg_filepath) as description_file:
+
+ new_areas = {}
+
+ # Load SVG path
+ for path in description_file.getElementsByTagName('path'):
+
+ # Convert d-string into array
+ d_string = path.getAttribute('d')
+
+ assert(d_string[0] == 'M')
+ assert(d_string[-1] == 'Z')
+
+ points = [(float(x), float(y)) for x, y in [p.split(',') for p in d_string[1:-1].split('L')]]
+
+ new_areas[path.getAttribute('id')] = AOIFeatures.AreaOfInterest(points)
+
+ # Load SVG rect
+ for rect in description_file.getElementsByTagName('rect'):
+
+ # Convert rect element into dict
+ rect_dict = {
+ 'shape': 'rectangle',
+ 'x': float(rect.getAttribute('x')),
+ 'y': float(rect.getAttribute('y')),
+ 'width': float(rect.getAttribute('width')),
+ 'height': float(rect.getAttribute('height'))
+ }
+
+ new_areas[rect.getAttribute('id')] = AOIFeatures.AreaOfInterest.from_dict(rect_dict)
+
+ # Load SVG circle
+ for circle in description_file.getElementsByTagName('circle'):
+
+ # Convert circle element into dict
+ circle_dict = {
+ 'shape': 'circle',
+ 'cx': float(circle.getAttribute('cx')),
+ 'cy': float(circle.getAttribute('cy')),
+ 'radius': float(circle.getAttribute('r'))
+ }
+
+ new_areas[circle.getAttribute('id')] = AOIFeatures.AreaOfInterest.from_dict(circle_dict)
+
+ return AOI2DScene(new_areas)
+
def draw(self, image: numpy.array, draw_aoi: dict = None, exclude=[]):
"""Draw AOI polygons on image.
diff --git a/src/argaze/AreaOfInterest/AOIFeatures.py b/src/argaze/AreaOfInterest/AOIFeatures.py
index ffaf882..debf1fa 100644
--- a/src/argaze/AreaOfInterest/AOIFeatures.py
+++ b/src/argaze/AreaOfInterest/AOIFeatures.py
@@ -11,6 +11,7 @@ from typing import TypeVar, Tuple
from dataclasses import dataclass, field
import json
import os
+import math
from argaze import DataStructures
@@ -41,6 +42,40 @@ class AreaOfInterest(numpy.ndarray):
return repr(self.tolist())
+ @classmethod
+ def from_dict(self, aoi_data: dict, working_directory: str = None) -> AreaOfInterestType:
+ """Load attributes from dictionary.
+
+ Parameters:
+ aoi_data: dictionary with attributes to load
+ working_directory: folder path where to load files when a dictionary value is a relative filepath.
+ """
+
+ shape = aoi_data.pop('shape')
+
+ if shape == 'rectangle':
+
+ x = aoi_data.pop('x')
+ y = aoi_data.pop('y')
+ width = aoi_data.pop('width')
+ height = aoi_data.pop('height')
+
+ points = [[x, y], [x+width, y], [x+width, y+height], [x, y+height]]
+
+ return AreaOfInterest(points)
+
+ elif shape == 'circle':
+
+ cx = aoi_data.pop('cx')
+ cy = aoi_data.pop('cy')
+ radius = aoi_data.pop('radius')
+
+ # TODO: Use pygeos
+ N = 32
+ points = [(math.cos(2*math.pi / N*x) * radius + cx, math.sin(2*math.pi / N*x) * radius + cy) for x in range(0, N+1)]
+
+ return AreaOfInterest(points)
+
@property
def dimension(self) -> int:
"""Number of axis coding area points positions."""
@@ -249,8 +284,15 @@ class AOIScene():
# Load areas
areas = {}
- for name, area in aoi_scene_data.items():
- areas[name] = AreaOfInterest(area)
+ for area_name, area_data in aoi_scene_data.items():
+
+ if type(area_data) == list:
+
+ areas[area_name] = AreaOfInterest(area_data)
+
+ elif type(area_data) == dict:
+
+ areas[area_name] = AreaOfInterest.from_dict(area_data)
# Default dimension is 0
dimension = 0
@@ -276,7 +318,7 @@ class AOIScene():
aoi_scene_data = json.load(configuration_file)
working_directory = os.path.dirname(json_filepath)
- return AOIScene.from_dict(aoi_scene_data, working_directory)
+ return AOIScene.from_dict(aoi_scene_data, working_directory)
def __getitem__(self, name) -> AreaOfInterest:
"""Get an AOI from the scene."""