From 61b098c152df19a27159ba66c2ce61dfaf0c44db Mon Sep 17 00:00:00 2001
From: Raphael <raphael.sturgis@gmail.com>
Date: Fri, 5 Aug 2022 14:17:40 +0200
Subject: [PATCH] data augmentation

---
 skais/learn/data_augmentation.py              |  64 +++++++++++
 .../random_axis_rotation_augmentor.py         |  20 ++++
 skais/tests/learn/__init__.py                 |   0
 skais/tests/learn/test_data_augmentation.py   |  56 ++++++++++
 skais/tests/utils/test_geometry.py            |  74 ++++++++++++-
 skais/utils/geography.py                      |   2 +-
 skais/utils/geometry.py                       | 100 ++++++++++++++++++
 7 files changed, 313 insertions(+), 3 deletions(-)
 create mode 100644 skais/learn/data_augmentation.py
 create mode 100644 skais/process/data_augmentation/random_axis_rotation_augmentor.py
 create mode 100644 skais/tests/learn/__init__.py
 create mode 100644 skais/tests/learn/test_data_augmentation.py

diff --git a/skais/learn/data_augmentation.py b/skais/learn/data_augmentation.py
new file mode 100644
index 0000000..13da4a4
--- /dev/null
+++ b/skais/learn/data_augmentation.py
@@ -0,0 +1,64 @@
+import random
+from copy import deepcopy
+
+import numpy as np
+from skais.utils.geometry import vectorize_angle, local_ref, transpose_matrix, \
+    quaternion_multiply, angleize_vector, rotation_quaternions, spherical_to_carthesian, carthesian_to_spherical, \
+    rotate_vector_by_quaternion
+
+base_ref = np.array(
+    [
+        [1, 0, 0],
+        [0, 1, 0],
+        [0, 0, 1]
+    ]
+)
+
+
+def shift_positions(aisPositions, axis_lat, axis_long, angle):
+    copy = deepcopy(aisPositions)
+    rotation_quaternion, inverse_rotation_quaternion = rotation_quaternions(axis_lat, axis_long, angle)
+
+    latitudes = np.radians(aisPositions.df['latitude'].to_numpy())
+    longitudes = np.radians(aisPositions.df['longitude'].to_numpy())
+    cogs = np.radians(aisPositions.df['cog'].to_numpy())
+    headings = np.radians(aisPositions.df['heading'].to_numpy())
+
+    for i in range(len(latitudes)):
+        lat = latitudes[i]
+        long = longitudes[i]
+        v_cog = vectorize_angle(cogs[i])
+        v_heading = vectorize_angle(headings[i])
+
+        new_position = rotate_vector_by_quaternion(spherical_to_carthesian(lat, long), rotation_quaternion, inverse_rotation_quaternion)
+        new_lat, new_long = carthesian_to_spherical(new_position)
+
+        origin_ref = local_ref(lat, long)
+        target_ref = local_ref(new_lat, new_long)
+
+        matrix_ob = transpose_matrix(origin_ref, base_ref)
+        matrix_bt = transpose_matrix(base_ref, target_ref)
+
+        # transpose vectors to base referential
+        v_cog = np.matmul(matrix_ob, v_cog)
+        v_heading = np.matmul(matrix_ob, v_heading)
+
+        # rotate vector
+        v_cog = rotate_vector_by_quaternion(v_cog, rotation_quaternion, inverse_rotation_quaternion)
+        v_heading = rotate_vector_by_quaternion(v_heading, rotation_quaternion, inverse_rotation_quaternion)
+
+        # transpose vector back to local referential of the new position
+        v_cog = np.matmul(matrix_bt, v_cog)
+        v_heading = np.matmul(matrix_bt, v_heading)
+
+        latitudes[i] = new_lat
+        longitudes[i] = new_long
+        cogs[i] = angleize_vector(v_cog)
+        headings[i] = angleize_vector(v_heading)
+
+    copy.df['latitude'] = np.degrees(latitudes)
+    copy.df['longitude'] = np.degrees(longitudes)
+    copy.df['cog'] = np.degrees(cogs)
+    copy.df['heading'] = np.degrees(headings)
+
+    return copy
diff --git a/skais/process/data_augmentation/random_axis_rotation_augmentor.py b/skais/process/data_augmentation/random_axis_rotation_augmentor.py
new file mode 100644
index 0000000..f10ebbd
--- /dev/null
+++ b/skais/process/data_augmentation/random_axis_rotation_augmentor.py
@@ -0,0 +1,20 @@
+import random
+import numpy as np
+from skais.learn.data_augmentation import shift_positions
+
+
+class RandomAxisRotationEngine:
+    def __init__(self, shifts_per_trajectory=1):
+        self.shifts_per_trajectory = shifts_per_trajectory
+
+    def apply(self, trajectories, shuffle=True):
+        new_trajectories = []
+        for trajectory in trajectories:
+            for _ in range(self.shifts_per_trajectory):
+                new_trajectories.append(shift_positions(trajectory, random.uniform(0, 1)*np.pi - np.pi/2,
+                                                        2*(random.uniform(0, 1)*np.pi) - np.pi,
+                                                        2*(random.uniform(0, 1)*np.pi) - np.pi))
+        if not shuffle:
+            return new_trajectories
+        else:
+            return random.shuffle(new_trajectories)
diff --git a/skais/tests/learn/__init__.py b/skais/tests/learn/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/skais/tests/learn/test_data_augmentation.py b/skais/tests/learn/test_data_augmentation.py
new file mode 100644
index 0000000..3626287
--- /dev/null
+++ b/skais/tests/learn/test_data_augmentation.py
@@ -0,0 +1,56 @@
+import unittest
+
+from skais.ais.ais_trajectory import AISTrajectory
+from skais.learn.data_augmentation import *
+import pandas as pd
+
+
+class data_augmentation(unittest.TestCase):
+    def test_shift(self):
+        trajectory = AISTrajectory(
+            pd.DataFrame(
+                {
+                    'ts_sec': [i for i in range(10)],
+                    'latitude': [0 for _ in range(5)] + [45 for _ in range(5)],
+                    'longitude': [0 for _ in range(10)],
+                    'cog': [0 for _ in range(10)],
+                    'heading': [90 for _ in range(10)]
+                }
+            )
+        )
+
+        shifted = shift_positions(trajectory, 0, 0, np.pi / 2)
+        self.assertTrue(True)
+
+    def test_shift_2(self):
+        trajectory = AISTrajectory(
+            pd.DataFrame(
+                {
+                    'ts_sec': [i for i in range(10)],
+                    'latitude': [0 for _ in range(10)],
+                    'longitude': [-150, -120, -60, -30, 0, 30, 60, 120, 150, 180],
+                    'cog': [0 for _ in range(10)],
+                    'heading': [90 for _ in range(10)]
+                }
+            )
+        )
+
+        shifted = shift_positions(trajectory, 0, 0, np.pi / 2)
+        self.assertTrue(True)
+
+    def test_shift_3(self):
+        trajectory = AISTrajectory(
+            pd.DataFrame(
+                {
+                    'ts_sec': [0],
+                    'latitude': [-45],
+                    'longitude': [45],
+                    'cog': [45],
+                    'heading': [120]
+                }
+            )
+        )
+
+        shifted = shift_positions(trajectory, np.pi/2, 0, np.pi / 2)
+        self.assertTrue(True)
+
diff --git a/skais/tests/utils/test_geometry.py b/skais/tests/utils/test_geometry.py
index 7a0b76d..da65b5e 100644
--- a/skais/tests/utils/test_geometry.py
+++ b/skais/tests/utils/test_geometry.py
@@ -1,6 +1,9 @@
 import unittest
 
-from skais.utils.geometry import bresenham
+import numpy as np
+
+from skais.process.geography import PI
+from skais.utils.geometry import bresenham, vectorize_angle, transpose_matrix, vector_rotation, local_ref
 
 
 class TestGeometry(unittest.TestCase):
@@ -18,7 +21,6 @@ class TestGeometry(unittest.TestCase):
 
         self.assertListEqual(result, expected)
 
-
     def test_bresenham_inverted_2(self):
         result = bresenham(16, 4, 3, 9)
         expected = [(3, 9), (4, 9), (5, 8), (6, 8), (7, 7), (8, 7), (9, 7), (10, 6), (11, 6), (12, 6), (13, 5), (14, 5),
@@ -37,6 +39,74 @@ class TestGeometry(unittest.TestCase):
 
         self.assertListEqual(result, expected)
 
+    def test_vectorize_angle_0(self):
+        vectorized_angle = vectorize_angle(0)
+        np.testing.assert_almost_equal(vectorized_angle, np.array([1, 0, 0]))
+
+    def test_vectorize_angle_half_PI(self):
+        vectorized_angle = vectorize_angle(PI / 2)
+        np.testing.assert_almost_equal(vectorized_angle, np.array([0, 1, 0]))
+
+    def test_vectorize_angle_PI(self):
+        vectorized_angle = vectorize_angle(PI)
+        np.testing.assert_almost_equal(vectorized_angle, np.array([-1, 0, 0]))
+
+    def test_transpose_matrix_1(self):
+        origin = np.array(
+            [[1, 0, 0],
+             [0, 1, 0],
+             [0, 0, 1]]
+        )
+        target = np.array(
+            [[1, 0, 0],
+             [0, 1, 0],
+             [0, 0, 1]]
+        )
+        result = transpose_matrix(origin, target)
+        expected = np.array(
+            [[1, 0, 0],
+             [0, 1, 0],
+             [0, 0, 1]]
+        )
+        np.testing.assert_almost_equal(result, expected)
+
+    def test_transpose_matrix_2(self):
+        origin = np.array(
+            [[1, 0, 0],
+             [0, 1, 0],
+             [0, 0, 1]]
+        )
+        target = np.array(
+            [[0, 1, 0],
+             [1, 0, 0],
+             [0, 0, 1]]
+        )
+        result = transpose_matrix(origin, target)
+        expected = np.array(
+            [[0, 1, 0],
+             [1, 0, 0],
+             [0, 0, 1]]
+        )
+        np.testing.assert_almost_equal(result, expected)
+
+    def test_rotate_vector(self):
+        result = vector_rotation(np.array([0, 0, 1]), 0, 0, np.pi / 2)
+
+        np.testing.assert_almost_equal(result, np.array([0, -1, 0]))
+
+    def test_local_ref_0(self):
+        result = local_ref(0, 0)
+
+        np.testing.assert_almost_equal(result, np.array([[0, 0, -1],
+                                                         [0, 1, 0],
+                                                         [1, 0, 0]]))
+
+    def test_local_ref_1(self):
+        result = local_ref(np.pi/2, 0)
+
+        np.testing.assert_almost_equal(result, np.array([[1, 0, 0],
+                                                         [0, 1, 0],
+                                                         [0, 0, 1]]))
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/skais/utils/geography.py b/skais/utils/geography.py
index ae539e1..99c6e28 100644
--- a/skais/utils/geography.py
+++ b/skais/utils/geography.py
@@ -4,7 +4,7 @@ from numba import jit
 R = 6371000
 
 
-@jit(nopython=True)
+@jit(nopython=True, fastmath=True)
 def great_circle(lat1, lat2, long1, long2):
     x1 = np.radians(lat1)
     y1 = np.radians(long1)
diff --git a/skais/utils/geometry.py b/skais/utils/geometry.py
index 66f0a29..4178db8 100644
--- a/skais/utils/geometry.py
+++ b/skais/utils/geometry.py
@@ -1,5 +1,7 @@
 import numpy as np
 from numba import jit
+import math as m
+
 
 @jit(nopython=True)
 def dist_on_grid(x1, y1, x2, y2):
@@ -65,3 +67,101 @@ def bresenham(x1, y1, x2, y2):
             pixels.append((x, y))
 
     return pixels
+
+@jit(nopython=True, cache=True)
+def vectorize_angle(angle):
+    return np.array([-np.cos(angle), np.sin(angle), 0])
+
+
+@jit(nopython=True, cache=True)
+def angleize_vector(vector):
+    if vector[2] > 0.0001:
+        raise ValueError
+    return np.arctan2(vector[1], -vector[0])
+
+
+@jit(nopython=True, cache=True)
+def local_ref(lat, long):
+    theta = (np.pi / 2) - lat
+    phi = long
+
+    return np.array(
+        [
+            [np.cos(theta) * np.cos(phi), np.cos(theta) * np.sin(phi), -np.sin(theta)],
+            [-np.sin(phi), np.cos(phi), 0],
+            [np.sin(theta) * np.cos(phi), np.sin(theta) * np.sin(phi), np.cos(theta)]
+        ]
+    )
+
+
+# @jit(nopython=True, cache=True)
+def transpose_matrix(origin_ref, target_ref):
+    i1 = origin_ref[0, :]
+    j1 = origin_ref[1, :]
+    k1 = origin_ref[2, :]
+    i2 = target_ref[0, :]
+    j2 = target_ref[1, :]
+    k2 = target_ref[2, :]
+
+    return np.array(
+        [
+            [i1.dot(i2), j1.dot( i2), k1.dot(i2)],
+            [i1.dot(j2), j1.dot(j2), k1.dot(j2)],
+            [i1.dot(k2), j1.dot(k2), k1.dot(k2)]
+        ]
+    )
+
+    # return np.matmul(origin_ref, target_ref)
+
+
+@jit(nopython=True, cache=True)
+def rotation_quaternions(lat, long, angle):
+    theta = (np.pi / 2) - lat
+    phi = long
+    vx = np.cos(phi) * np.sin(theta)
+    vy = np.sin(phi) * np.sin(theta)
+    vz = np.cos(theta)
+
+    return (np.array([np.cos(angle / 2), vx * np.sin(angle / 2), vy * np.sin(angle / 2), vz * np.sin(angle / 2)]),
+            np.array([np.cos(angle / 2), -vx * np.sin(angle / 2), -vy * np.sin(angle / 2), -vz * np.sin(angle / 2)]))
+
+
+@jit(nopython=True, cache=True)
+def quaternion_multiply(quaternion0, quaternion1):
+    w0, x0, y0, z0 = quaternion0
+    w1, x1, y1, z1 = quaternion1
+    return np.array([(w0 * w1) - (x0 * x1) - (y0 * y1) - (z0 * z1),
+                     (w0 * x1) + (x0 * w1) + (y0 * z1) - (z0 * y1),
+                     (w0 * y1) - (x0 * z1) + (y0 * w1) + (z0 * x1),
+                     (w0 * z1) + (x0 * y1) - (y0 * x1) + (z0 * w1)])
+
+
+@jit(nopython=True, cache=True)
+def vector_rotation(vect, lat, long, angle):
+    q, q_conjugate = rotation_quaternions(lat, long, angle)
+    v = np.insert(vect, 0, 0)
+
+    return quaternion_multiply(quaternion_multiply(q, v), q_conjugate)[1:]
+
+
+@jit(nopython=True, cache=True)
+def spherical_to_carthesian(lat, long):
+    theta = (np.pi / 2) - lat
+    phi = long
+    return np.array([np.cos(phi) * np.sin(theta), np.sin(phi) * np.sin(theta), np.cos(theta)])
+
+
+@jit(nopython=True, cache=True)
+def carthesian_to_spherical(vect):
+    x = vect[0]
+    y = vect[1]
+    z = vect[2]
+    XsqPlusYsq = x ** 2 + y ** 2
+    return m.atan2(z, m.sqrt(XsqPlusYsq)), m.atan2(y, x)
+
+
+@jit(nopython=True, cache=True)
+def rotate_vector_by_quaternion(vector, quaternion, inverse_quaternion):
+    vect = np.zeros(4)
+    vect[1:] = vector
+    return quaternion_multiply(quaternion_multiply(quaternion, vect), inverse_quaternion)[1:]
-- 
GitLab