From fc7bac69c96cd1b03767c56ebf55d26f3b0d7376 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 16 Oct 2022 21:13:33 +0100 Subject: [PATCH 01/10] Clarify that this 2D type is for pixels --- docs/coordinates.rst | 6 +++--- tests/test_module.py | 2 +- zoloto/__init__.py | 4 ++-- zoloto/coords.py | 7 ++++++- zoloto/marker.py | 12 +++++++----- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/coordinates.rst b/docs/coordinates.rst index 4b8988e1..9e6778f2 100644 --- a/docs/coordinates.rst +++ b/docs/coordinates.rst @@ -6,9 +6,9 @@ Orientation .. autoclass:: zoloto.coords.Orientation :members: -Coordinates ------------ -.. autoclass:: zoloto.coords.Coordinates +PixelCoordinates +---------------- +.. autoclass:: zoloto.coords.PixelCoordinates :members: :no-inherited-members: diff --git a/tests/test_module.py b/tests/test_module.py index d2dd5c79..18a96609 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -20,7 +20,7 @@ def test_exposes_marker_type() -> None: @pytest.mark.parametrize( "coordinate_struct", - ["Coordinates", "Orientation", "ThreeDCoordinates", "Spherical"], + ["PixelCoordinates", "Orientation", "ThreeDCoordinates", "Spherical"], ) def test_exposes_coordinates(coordinate_struct: str) -> None: assert getattr(zoloto, coordinate_struct) == getattr( diff --git a/zoloto/__init__.py b/zoloto/__init__.py index 04ab784f..aadb5258 100644 --- a/zoloto/__init__.py +++ b/zoloto/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from zoloto.coords import Coordinates, Orientation, Spherical, ThreeDCoordinates +from zoloto.coords import Orientation, PixelCoordinates, Spherical, ThreeDCoordinates from zoloto.marker import Marker from zoloto.marker_type import MarkerType @@ -8,8 +8,8 @@ __all__ = [ - "Coordinates", "Orientation", + "PixelCoordinates", "Spherical", "ThreeDCoordinates", "Marker", diff --git a/zoloto/coords.py b/zoloto/coords.py index 8f8f4285..168bb893 100644 --- a/zoloto/coords.py +++ b/zoloto/coords.py @@ -7,8 +7,13 @@ from pyquaternion import Quaternion -class Coordinates(NamedTuple): +class PixelCoordinates(NamedTuple): """ + Coordinates within an image made up from pixels. + + This type allows float values to account for computed locations which are + not limited to exact pixel boundaries. + :param float x: X coordinate :param float y: Y coordinate """ diff --git a/zoloto/marker.py b/zoloto/marker.py index dfc70218..3f7bed0a 100644 --- a/zoloto/marker.py +++ b/zoloto/marker.py @@ -11,7 +11,7 @@ from zoloto.utils import cached_method from .calibration import CalibrationParameters -from .coords import Coordinates, Orientation, Spherical, ThreeDCoordinates +from .coords import Orientation, PixelCoordinates, Spherical, ThreeDCoordinates from .exceptions import MissingCalibrationsError from .marker_type import MarkerType @@ -45,13 +45,15 @@ def marker_type(self) -> MarkerType: return self.__marker_type @property - def pixel_corners(self) -> list[Coordinates]: - return [Coordinates(x=float(x), y=float(y)) for x, y in self._pixel_corners] + def pixel_corners(self) -> list[PixelCoordinates]: + return [ + PixelCoordinates(x=float(x), y=float(y)) for x, y in self._pixel_corners + ] @cached_property - def pixel_centre(self) -> Coordinates: + def pixel_centre(self) -> PixelCoordinates: tl, _, br, _ = self.pixel_corners - return Coordinates( + return PixelCoordinates( x=tl.x + (self.size / 2) - 1, y=br.y - (self.size / 2), ) From 14f1da75fe797711106ce3e2c3add29d2990dce5 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 16 Oct 2022 21:29:17 +0100 Subject: [PATCH 02/10] Clarify that these are cartesian 3D coordinates It would be great to document the orientation of the axes etc., however that's already in flight in https://github.com/RealOrangeOne/zoloto/pull/302 so leaving that for now. --- docs/coordinates.rst | 6 +++--- tests/test_module.py | 2 +- zoloto/__init__.py | 4 ++-- zoloto/coords.py | 2 +- zoloto/marker.py | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/coordinates.rst b/docs/coordinates.rst index 9e6778f2..8513a012 100644 --- a/docs/coordinates.rst +++ b/docs/coordinates.rst @@ -12,9 +12,9 @@ PixelCoordinates :members: :no-inherited-members: -ThreeDCoordinates ------------------ -.. autoclass:: zoloto.coords.ThreeDCoordinates +CartesianCoordinates +-------------------- +.. autoclass:: zoloto.coords.CartesianCoordinates :members: :no-inherited-members: diff --git a/tests/test_module.py b/tests/test_module.py index 18a96609..67d10dfb 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -20,7 +20,7 @@ def test_exposes_marker_type() -> None: @pytest.mark.parametrize( "coordinate_struct", - ["PixelCoordinates", "Orientation", "ThreeDCoordinates", "Spherical"], + ["PixelCoordinates", "Orientation", "CartesianCoordinates", "Spherical"], ) def test_exposes_coordinates(coordinate_struct: str) -> None: assert getattr(zoloto, coordinate_struct) == getattr( diff --git a/zoloto/__init__.py b/zoloto/__init__.py index aadb5258..056428ab 100644 --- a/zoloto/__init__.py +++ b/zoloto/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from zoloto.coords import Orientation, PixelCoordinates, Spherical, ThreeDCoordinates +from zoloto.coords import CartesianCoordinates, Orientation, PixelCoordinates, Spherical from zoloto.marker import Marker from zoloto.marker_type import MarkerType @@ -8,10 +8,10 @@ __all__ = [ + "CartesianCoordinates", "Orientation", "PixelCoordinates", "Spherical", - "ThreeDCoordinates", "Marker", "MarkerType", ] diff --git a/zoloto/coords.py b/zoloto/coords.py index 168bb893..4019b682 100644 --- a/zoloto/coords.py +++ b/zoloto/coords.py @@ -22,7 +22,7 @@ class PixelCoordinates(NamedTuple): y: float -class ThreeDCoordinates(NamedTuple): +class CartesianCoordinates(NamedTuple): """ :param float x: X coordinate :param float y: Y coordinate diff --git a/zoloto/marker.py b/zoloto/marker.py index 3f7bed0a..9852c543 100644 --- a/zoloto/marker.py +++ b/zoloto/marker.py @@ -11,7 +11,7 @@ from zoloto.utils import cached_method from .calibration import CalibrationParameters -from .coords import Orientation, PixelCoordinates, Spherical, ThreeDCoordinates +from .coords import CartesianCoordinates, Orientation, PixelCoordinates, Spherical from .exceptions import MissingCalibrationsError from .marker_type import MarkerType @@ -74,8 +74,8 @@ def spherical(self) -> Spherical: ) @property - def cartesian(self) -> ThreeDCoordinates: - return ThreeDCoordinates(*self._tvec.tolist()) + def cartesian(self) -> CartesianCoordinates: + return CartesianCoordinates(*self._tvec.tolist()) @property def _rvec(self) -> NDArray: From 20cc514e93f319e54a690c4952b82a9239ab815f Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 16 Oct 2022 21:32:59 +0100 Subject: [PATCH 03/10] Clarify that these are Spherical Coordinates It would be great to document the orientation of the axes etc., however that's already in flight in https://github.com/RealOrangeOne/zoloto/pull/302 so leaving that for now. --- docs/coordinates.rst | 6 +++--- tests/test_module.py | 2 +- zoloto/__init__.py | 9 +++++++-- zoloto/coords.py | 2 +- zoloto/marker.py | 11 ++++++++--- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/docs/coordinates.rst b/docs/coordinates.rst index 8513a012..587a4f7e 100644 --- a/docs/coordinates.rst +++ b/docs/coordinates.rst @@ -18,9 +18,9 @@ CartesianCoordinates :members: :no-inherited-members: -Spherical ---------- -.. autoclass:: zoloto.coords.Spherical +SphericalCoordinates +-------------------- +.. autoclass:: zoloto.coords.SphericalCoordinates :members: :no-inherited-members: diff --git a/tests/test_module.py b/tests/test_module.py index 67d10dfb..66c8bf8a 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -20,7 +20,7 @@ def test_exposes_marker_type() -> None: @pytest.mark.parametrize( "coordinate_struct", - ["PixelCoordinates", "Orientation", "CartesianCoordinates", "Spherical"], + ["PixelCoordinates", "Orientation", "CartesianCoordinates", "SphericalCoordinates"], ) def test_exposes_coordinates(coordinate_struct: str) -> None: assert getattr(zoloto, coordinate_struct) == getattr( diff --git a/zoloto/__init__.py b/zoloto/__init__.py index 056428ab..8a1e0e0f 100644 --- a/zoloto/__init__.py +++ b/zoloto/__init__.py @@ -1,6 +1,11 @@ from __future__ import annotations -from zoloto.coords import CartesianCoordinates, Orientation, PixelCoordinates, Spherical +from zoloto.coords import ( + CartesianCoordinates, + Orientation, + PixelCoordinates, + SphericalCoordinates, +) from zoloto.marker import Marker from zoloto.marker_type import MarkerType @@ -11,7 +16,7 @@ "CartesianCoordinates", "Orientation", "PixelCoordinates", - "Spherical", + "SphericalCoordinates", "Marker", "MarkerType", ] diff --git a/zoloto/coords.py b/zoloto/coords.py index 4019b682..89c0df04 100644 --- a/zoloto/coords.py +++ b/zoloto/coords.py @@ -34,7 +34,7 @@ class CartesianCoordinates(NamedTuple): z: float -class Spherical(NamedTuple): +class SphericalCoordinates(NamedTuple): """ :param float rot_x: Rotation around the X-axis, in radians :param float rot_y: Rotation around the Y-axis, in radians diff --git a/zoloto/marker.py b/zoloto/marker.py index 9852c543..9397a403 100644 --- a/zoloto/marker.py +++ b/zoloto/marker.py @@ -11,7 +11,12 @@ from zoloto.utils import cached_method from .calibration import CalibrationParameters -from .coords import CartesianCoordinates, Orientation, PixelCoordinates, Spherical +from .coords import ( + CartesianCoordinates, + Orientation, + PixelCoordinates, + SphericalCoordinates, +) from .exceptions import MissingCalibrationsError from .marker_type import MarkerType @@ -67,9 +72,9 @@ def orientation(self) -> Orientation: return Orientation(*self._rvec) @cached_property - def spherical(self) -> Spherical: + def spherical(self) -> SphericalCoordinates: x, y, z = self._tvec - return Spherical( + return SphericalCoordinates( rot_x=float(arctan2(y, z)), rot_y=float(arctan2(x, z)), dist=self.distance ) From 84b3d12f2d988b047668912734ebb4fc3a819877 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Tue, 18 Oct 2022 17:06:37 +0100 Subject: [PATCH 04/10] Document the nature of the orientation rotations This was determined by experimentation with an old printed marker I had to hand. There's some slight oddness here in that the values suggest the marker I have is upside down, however that seems to disagree with other sources I've checked for the same marker. Also worth noting that I don't have a calibration for the camera I used, so this is based off another calibration I found for the same resolution. --- zoloto/coords.py | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/zoloto/coords.py b/zoloto/coords.py index 89c0df04..ed0fca53 100644 --- a/zoloto/coords.py +++ b/zoloto/coords.py @@ -64,17 +64,58 @@ def __init__(self, e_x: float, e_y: float, e_z: float): @property def rot_x(self) -> float: - """Get rotation angle around x axis in radians.""" + """ + Get rotation angle around X axis in radians. + + The X axis is horizontal relative to the camera's perspective, i.e: left + & right within the frame of the image. + + Increasing values represent an increasing clockwise rotation of the + marker as seen from the camera's left. + + Zero values for April Tags markers have the marker facing away from the + camera. The practical effect of this is that an April Tags marker facing + the camera square-on will have a value of ``pi`` (or equivalently + ``-pi``) and the value will decrease as the marker diverges from + square-on. + + For observed markers positive values therefore indicate a rotation of + the top of the marker away from the camera, such that marker could be + said to be leaning backwards, with the value decreasing as the marker + leans back further. + """ return self.roll @property def rot_y(self) -> float: - """Get rotation angle around y axis in radians.""" + """ + Get rotation angle around Y axis in radians. + + The Y axis is vertical relative to the camera's perspective, i.e: up & + down within the frame of the image. + + Positive values indicate a rotation of an observed marker towards the + camera's right. This is a rotation of the marker counter-clockwise about + the Y axis as seen from above the marker. + + Zero values for April Tags markers have the marker facing the camera + square-on. + """ return self.pitch @property def rot_z(self) -> float: - """Get rotation angle around z axis in radians.""" + """ + Get rotation angle around Z axis in radians. + + The Z axis extends directly away from the camera. + + Positive values indicate a rotation counter-clockwise from the + perspective of the camera. + + Zero values for April Tags markers have the marker reference point at + the top left. + """ return self.yaw @property From 7e59e776f9d0f0252fe85c47ff32621854d511bc Mon Sep 17 00:00:00 2001 From: Peter Law Date: Tue, 18 Oct 2022 17:29:20 +0100 Subject: [PATCH 05/10] Document the nature of the cartesian coordinates --- zoloto/coords.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/zoloto/coords.py b/zoloto/coords.py index ed0fca53..1b3d5708 100644 --- a/zoloto/coords.py +++ b/zoloto/coords.py @@ -24,6 +24,22 @@ class PixelCoordinates(NamedTuple): class CartesianCoordinates(NamedTuple): """ + Cartesian coordinates, rotated on their side. + + The X axis is horizontal relative to the camera's perspective, i.e: left & + right within the frame of the image. Zero is at the centre of the image. + Increasing values indicate greater distance to the right. + + The Y axis is vertical relative to the camera's perspective, i.e: up & down + within the frame of the image. Zero is at the centre of the image. + Increasing values indicate greater distance below the centre of the image. + + The Z axis extends directly away from the camera. Zero is at the camera. + Increasing values indicate greater distance from the camera. + + These match traditional cartesian coordinates when the camera is facing + upwards. + :param float x: X coordinate :param float y: Y coordinate :param float z: Z coordinate From 7aa295bae7ac913d5e69630984d8731ecccd4405 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Tue, 18 Oct 2022 17:45:58 +0100 Subject: [PATCH 06/10] Document the spherical coordinates Derived from testing of the current system. This includes moving the construction of the coordinates into the type as that is functionally where the definition of the nature of the spherical coordinates comes from. --- zoloto/coords.py | 29 +++++++++++++++++++++++++++-- zoloto/marker.py | 7 ++----- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/zoloto/coords.py b/zoloto/coords.py index 1b3d5708..85ef5260 100644 --- a/zoloto/coords.py +++ b/zoloto/coords.py @@ -1,5 +1,6 @@ from __future__ import annotations +import math from typing import Iterator, NamedTuple, Tuple from cached_property import cached_property @@ -52,8 +53,22 @@ class CartesianCoordinates(NamedTuple): class SphericalCoordinates(NamedTuple): """ - :param float rot_x: Rotation around the X-axis, in radians - :param float rot_y: Rotation around the Y-axis, in radians + Twin-angle + distance coordinates, from the perspective of the camera. + + These are not spherical coordinates in the typical sense as each of the + angles are computed separately rather than being applicable one after the + other. This results in some real world positions being considered equal by + this type, however such positions are expected to be out of the viewable + world for a standard camera. + + The axes definitions are as for ``CartesianCoordinates``. + + :param float rot_x: Rotation around the X-axis, in radians. Zero is at the + centre of the image. Increasing values indicate greater distance towards + the bottom of the image. + :param float rot_y: Rotation around the Y-axis, in radians. Zero is at the + centre of the image. Increasing values indicate greater distance to the + right within the image. :param float dist: Distance """ @@ -61,6 +76,16 @@ class SphericalCoordinates(NamedTuple): rot_y: float dist: int + @classmethod + def from_cartesian(cls, cartesian: CartesianCoordinates) -> SphericalCoordinates: + distance = math.sqrt(sum(x**2 for x in cartesian)) + x, y, z = cartesian + return cls( + rot_x=math.atan2(y, z), + rot_y=math.atan2(x, z), + dist=int(distance), + ) + ThreeTuple = Tuple[float, float, float] RotationMatrix = Tuple[ThreeTuple, ThreeTuple, ThreeTuple] diff --git a/zoloto/marker.py b/zoloto/marker.py index 9397a403..6579ef44 100644 --- a/zoloto/marker.py +++ b/zoloto/marker.py @@ -5,7 +5,7 @@ from cached_property import cached_property from cv2 import aruco -from numpy import arctan2, linalg +from numpy import linalg from numpy.typing import NDArray from zoloto.utils import cached_method @@ -73,10 +73,7 @@ def orientation(self) -> Orientation: @cached_property def spherical(self) -> SphericalCoordinates: - x, y, z = self._tvec - return SphericalCoordinates( - rot_x=float(arctan2(y, z)), rot_y=float(arctan2(x, z)), dist=self.distance - ) + return SphericalCoordinates.from_cartesian(self.cartesian) @property def cartesian(self) -> CartesianCoordinates: From 6294d47daded7d7560e388d2b6aa70647356099c Mon Sep 17 00:00:00 2001 From: Peter Law Date: Tue, 18 Oct 2022 17:55:32 +0100 Subject: [PATCH 07/10] Provide tests for Spherical & Cartesian coordinate conversions This helps understand the relationship between these and ensures that changes aren't accidental. --- tests/test_coords.py | 59 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/tests/test_coords.py b/tests/test_coords.py index 5dbee145..9ef6e90f 100644 --- a/tests/test_coords.py +++ b/tests/test_coords.py @@ -1,11 +1,14 @@ """Tests for coordinates classes.""" from __future__ import annotations +import math + +import pytest from hypothesis import given from hypothesis.strategies import floats, tuples from pyquaternion import Quaternion -from zoloto.coords import Orientation +from zoloto.coords import CartesianCoordinates, Orientation, SphericalCoordinates @given(tuples(floats(), floats(), floats())) @@ -62,3 +65,57 @@ def test_repr(euler_angles: tuple[float, float, float]) -> None: for name, val in zip(names, ypr): assert f"{name}={val}" in repr_str + + +@pytest.mark.parametrize( + "cartesian,expected", + [ + pytest.param( + CartesianCoordinates(0, 0, 0), + SphericalCoordinates(0, 0, 0), + id="origin", + ), + pytest.param( + CartesianCoordinates(0, 0, 1), + SphericalCoordinates( + rot_x=0, + rot_y=0, + dist=1, + ), + id="in-front-of-you", + ), + pytest.param( + CartesianCoordinates(0, 1, 0), + SphericalCoordinates( + rot_x=math.pi / 2, + rot_y=0, + dist=1, + ), + id="above-you", + ), + pytest.param( + CartesianCoordinates(1, 0, 0), + SphericalCoordinates( + rot_x=0, + rot_y=math.pi / 2, + dist=1, + ), + id="to-one-side", + ), + pytest.param( + CartesianCoordinates(1000, 1000, 0), + SphericalCoordinates( + rot_x=math.pi / 2, + rot_y=math.pi / 2, + dist=1414, + ), + id="to-one-side-and-up", + ), + ], +) +def test_spherical_from_cartesian( + cartesian: CartesianCoordinates, + expected: SphericalCoordinates, +) -> None: + spherical = SphericalCoordinates.from_cartesian(cartesian) + assert spherical == expected From d58cd69dfea201eca08cb341071db5e99d2e598f Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 9 Oct 2022 18:42:50 +0100 Subject: [PATCH 08/10] Make the Spherical coordinates useful This changes the Spherical coordinates to (almost) following the ISO convention. Consequently there are a number of changes to the values, notably: - the reference positions for each angle changes - the interpretation of the angles changes See inline documentation for the interpretation of the values now. Fixes https://github.com/RealOrangeOne/zoloto/issues/300 --- tests/test_coords.py | 24 +++++++++--------- tests/test_marker.py | 9 ++++--- zoloto/coords.py | 60 ++++++++++++++++++++++++++------------------ 3 files changed, 53 insertions(+), 40 deletions(-) diff --git a/tests/test_coords.py b/tests/test_coords.py index 9ef6e90f..35722845 100644 --- a/tests/test_coords.py +++ b/tests/test_coords.py @@ -78,36 +78,36 @@ def test_repr(euler_angles: tuple[float, float, float]) -> None: pytest.param( CartesianCoordinates(0, 0, 1), SphericalCoordinates( - rot_x=0, - rot_y=0, - dist=1, + theta=math.pi / 2, + phi=math.pi / 2, + distance=1, ), id="in-front-of-you", ), pytest.param( CartesianCoordinates(0, 1, 0), SphericalCoordinates( - rot_x=math.pi / 2, - rot_y=0, - dist=1, + theta=0, + phi=0, + distance=1, ), id="above-you", ), pytest.param( CartesianCoordinates(1, 0, 0), SphericalCoordinates( - rot_x=0, - rot_y=math.pi / 2, - dist=1, + theta=math.pi / 2, + phi=0, + distance=1, ), id="to-one-side", ), pytest.param( CartesianCoordinates(1000, 1000, 0), SphericalCoordinates( - rot_x=math.pi / 2, - rot_y=math.pi / 2, - dist=1414, + theta=0.7853981633974484, # math.pi / 4, with floating point error + phi=0, + distance=1414, ), id="to-one-side-and-up", ), diff --git a/tests/test_marker.py b/tests/test_marker.py index d6777d63..62700940 100644 --- a/tests/test_marker.py +++ b/tests/test_marker.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import math from typing import Any from unittest import TestCase from unittest.mock import patch @@ -71,10 +72,10 @@ def test_cartesian_coordinates(self) -> None: self.assertAlmostEqual(int(z), 910, delta=100) # HACK: Sometimes it changes def test_spherical_coordinates(self) -> None: - rot_x, rot_y, dist = self.marker.spherical + dist, rot_x, rot_y = self.marker.spherical self.assertEqual(dist, self.marker.distance) - self.assertAlmostEqual(rot_x, 0, delta=0.1) - self.assertAlmostEqual(rot_y, 0, delta=0.1) + self.assertAlmostEqual(rot_x, math.pi / 2, delta=0.1) + self.assertAlmostEqual(rot_y, math.pi / 2, delta=0.1) def test_as_dict(self) -> None: marker_dict = self.marker.as_dict() @@ -119,7 +120,7 @@ def test_marker_types(self) -> None: self.assertIsType(self.marker.spherical.rot_x, float) self.assertIsType(self.marker.spherical.rot_y, float) - self.assertIsType(self.marker.spherical.dist, int) + self.assertIsType(self.marker.spherical.distance, int) self.assertIsType(self.marker.cartesian.x, float) self.assertIsType(self.marker.cartesian.y, float) diff --git a/zoloto/coords.py b/zoloto/coords.py index 85ef5260..62324f92 100644 --- a/zoloto/coords.py +++ b/zoloto/coords.py @@ -53,37 +53,49 @@ class CartesianCoordinates(NamedTuple): class SphericalCoordinates(NamedTuple): """ - Twin-angle + distance coordinates, from the perspective of the camera. - - These are not spherical coordinates in the typical sense as each of the - angles are computed separately rather than being applicable one after the - other. This results in some real world positions being considered equal by - this type, however such positions are expected to be out of the viewable - world for a standard camera. - - The axes definitions are as for ``CartesianCoordinates``. - - :param float rot_x: Rotation around the X-axis, in radians. Zero is at the - centre of the image. Increasing values indicate greater distance towards - the bottom of the image. - :param float rot_y: Rotation around the Y-axis, in radians. Zero is at the - centre of the image. Increasing values indicate greater distance to the - right within the image. - :param float dist: Distance + SphericalCoordinates coordinates, rotated onto their side. + + This is comparable to the ISO convention for spherical coordinates, applied + to our rotated axes. Here θ is measured down from the y-axis (rather than + the usual z-axis) while φ is measured around the y-axis. + + See https://en.wikipedia.org/wiki/Spherical_coordinate_system and + https://studentrobotics.org/docs/programming/sr/vision/#SphericalCoordinates. + + :param float distance: Radial distance from the origin. + :param float theta: Polar angle, θ, in radians. This is the angle "down" + from the y-axis to the vector which points to the location. For points + with zero cartesian x-coordinate value, this can be viewed as the + rotation about the x-axis. Zero is on the positive y-axis. + :param float phi: Azimuth angle, φ, in radians. This is the angle from the + x-axis around the polar (y-axis) to the projection of the point on the + x-z plane. This can be viewed as rotation about the y-axis. Zero is at + the centre of the image. """ - rot_x: float - rot_y: float - dist: int + distance: int + theta: float + phi: float + + @property + def rot_x(self) -> float: + return self.theta + + @property + def rot_y(self) -> float: + return self.phi @classmethod def from_cartesian(cls, cartesian: CartesianCoordinates) -> SphericalCoordinates: + if not any(cartesian): + return SphericalCoordinates(0, 0, 0) + distance = math.sqrt(sum(x**2 for x in cartesian)) x, y, z = cartesian - return cls( - rot_x=math.atan2(y, z), - rot_y=math.atan2(x, z), - dist=int(distance), + return SphericalCoordinates( + distance=int(distance), + theta=math.acos(y / distance), + phi=math.atan2(z, x), ) From e196dfa87f830ba1de50a162b968998bee0d9da8 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Sun, 16 Oct 2022 20:58:18 +0100 Subject: [PATCH 09/10] Use a consistent approach for marker distance --- zoloto/marker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zoloto/marker.py b/zoloto/marker.py index 6579ef44..88248e60 100644 --- a/zoloto/marker.py +++ b/zoloto/marker.py @@ -5,7 +5,6 @@ from cached_property import cached_property from cv2 import aruco -from numpy import linalg from numpy.typing import NDArray from zoloto.utils import cached_method @@ -65,7 +64,7 @@ def pixel_centre(self) -> PixelCoordinates: @cached_property def distance(self) -> int: - return int(linalg.norm(self._tvec)) + return self.spherical.distance @cached_property def orientation(self) -> Orientation: From 98dd517dccd337cfe053264aab227d3af1792be3 Mon Sep 17 00:00:00 2001 From: Peter Law Date: Mon, 17 Oct 2022 21:32:09 +0100 Subject: [PATCH 10/10] Add docstrings to placate Sphinx --- zoloto/coords.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zoloto/coords.py b/zoloto/coords.py index 62324f92..893abb63 100644 --- a/zoloto/coords.py +++ b/zoloto/coords.py @@ -79,10 +79,12 @@ class SphericalCoordinates(NamedTuple): @property def rot_x(self) -> float: + """Approximate rotation around the x-axis, an alias for ``self.theta``.""" return self.theta @property def rot_y(self) -> float: + """Rotation around the y-axis, an alias for ``self.phi``.""" return self.phi @classmethod