Skip to content

Commit 36b545f

Browse files
committed
xyzwpr implementation
1 parent a4594c6 commit 36b545f

File tree

8 files changed

+248
-93
lines changed

8 files changed

+248
-93
lines changed

Diff for: .gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.idea/
22
venv/
33
__pycache__/
4+
.pytest_cache/
45

56
*.pyc

Diff for: python/generate_tests.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import json
2+
from pathlib import Path
3+
import numpy
4+
5+
raw_data = """
6+
{ -0.355708, -0.173709, 0.918312, 0.000000, 0.717742, -0.680099, 0.149368, 0.000000, 0.598597, 0.712243, 0.366595, 0.000000, 250.689172, 190.092170, 377.021336, 1.000000 }, new double[] { 250.689172, 190.092170, 377.021336, 22.168324, -66.680543, -153.971487 }) };
7+
{ 0.875453, 0.474534, 0.091654, 0.000000, 0.335633, -0.733383, 0.591185, 0.000000, 0.347755, -0.486792, -0.801312, 0.000000, 324.515443, -269.225543, 109.845856, 1.000000 }, new double[] { 324.515443, -269.225543, 109.845856, 143.581155, -5.258767, 28.459638 }) };
8+
{ 0.829857, -0.392800, 0.396290, 0.000000, -0.290899, -0.910637, -0.293458, 0.000000, 0.476147, 0.128247, -0.869964, 0.000000, 480.998803, -24.848409, 44.451180, 1.000000 }, new double[] { 480.998803, -24.848409, 44.451180, -161.359637, -23.346471, -25.329844 }) };
9+
{ 0.622543, 0.286126, 0.728404, 0.000000, 0.659905, -0.692256, -0.292073, 0.000000, 0.420672, 0.662506, -0.619775, 0.000000, 427.425001, 317.808911, 185.181686, 1.000000 }, new double[] { 427.425001, 317.808911, 185.181686, -154.767540, -46.752739, 24.683898 }) };
10+
{ 0.121273, 0.027579, 0.992236, 0.000000, 0.016312, -0.999534, 0.025788, 0.000000, 0.992485, 0.013058, -0.121667, 0.000000, 640.024599, 54.109546, 303.808089, 1.000000 }, new double[] { 640.024599, 54.109546, 303.808089, 168.032738, -82.855635, 12.811951 }) };
11+
{ -0.155532, 0.025165, 0.987510, 0.000000, -0.422104, -0.905508, -0.043406, 0.000000, 0.893106, -0.423583, 0.151457, 0.000000, 416.744163, -205.770414, 328.606008, 1.000000 }, new double[] { 416.744163, -205.770414, 328.606008, -15.991663, -80.935040, 170.809286 }) };
12+
{ -0.027822, -0.281693, 0.959101, 0.000000, -0.180557, -0.942275, -0.281989, 0.000000, 0.983171, -0.181018, -0.024646, 0.000000, 431.202555, -21.951723, 228.946720, 1.000000 }, new double[] { 431.202555, -21.951723, 228.946720, -94.994930, -73.556872, -95.640661 }) };
13+
{ -0.779100, 0.547427, 0.305495, 0.000000, -0.238741, -0.709679, 0.662841, 0.000000, 0.579661, 0.443485, 0.683604, 0.000000, 243.109205, 126.817247, 442.214652, 1.000000 }, new double[] { 243.109205, 126.817247, 442.214652, 44.116508, -17.787954, 144.906558 }) };
14+
{ -0.338094, 0.151820, 0.928786, 0.000000, -0.333176, -0.942296, 0.032746, 0.000000, 0.880162, -0.298378, 0.369167, 0.000000, 342.415569, -7.408160, 346.902494, 1.000000 }, new double[] { 342.415569, -7.408160, 346.902494, 5.069014, -68.246332, 155.817742 }) };
15+
16+
"""
17+
18+
19+
def main():
20+
print("@pytest.mark.parametrize('matrix,xyzwpr', [")
21+
for line in raw_data.split("\n"):
22+
if not line.strip():
23+
continue
24+
25+
start, end = line.split("},")
26+
start = _as_list(start.split("{")[1].strip())
27+
end = _as_list(end.split("{")[1].strip().split("}")[0].strip())
28+
29+
t = numpy.matrix(start).reshape(4, 4).transpose().tolist()
30+
print(f" ({t}, {end}),")
31+
print("])")
32+
33+
# source_file = Path.cwd().parent / "test_cases" / "r30ib_forward_kinematics.json"
34+
# with source_file.open("r") as handle:
35+
# data = json.load(handle)
36+
#
37+
# print(data)
38+
39+
40+
def _as_list(text: str) -> list[float]:
41+
return [float(x.strip()) for x in text.split(",")]
42+
43+
44+
if __name__ == '__main__':
45+
main()

Diff for: python/lrmate/robot.py

-92
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,8 @@
1313
import math
1414
import os
1515
from typing import List, Optional, Dict, Union
16-
from dataclasses import dataclass
1716
from pathlib import Path
1817

19-
import numpy
20-
from scipy.spatial.transform import Rotation
2118

2219
from open3d import geometry
2320
import open3d
@@ -152,92 +149,3 @@ def link_meshes(self) -> List[open3d.geometry.TriangleMesh]:
152149
return [link.mesh for link in self.links.values()]
153150

154151

155-
@dataclass
156-
class Vector:
157-
x: float
158-
y: float
159-
z: float
160-
161-
def __add__(self, other):
162-
return Vector(self.x + other.x, self.y + other.y, self.z + other.z)
163-
164-
def __sub__(self, other):
165-
return Vector(self.x - other.x, self.y - other.y, self.z - other.z)
166-
167-
def __mul__(self, other: Union[float, Transform]) -> Vector:
168-
if isinstance(other, float):
169-
return Vector(self.x * other, self.y * other, self.z * other)
170-
elif isinstance(other, Transform):
171-
moved = other * numpy.array([self.x, self.y, self.z, 1])
172-
return Vector(moved[0], moved[1], moved[2])
173-
else:
174-
raise TypeError("Can only multiply by scalar")
175-
176-
def __truediv__(self, other: float) -> Vector:
177-
return Vector(self.x / other, self.y / other, self.z / other)
178-
179-
def norm(self):
180-
return numpy.sqrt(self.x**2 + self.y**2 + self.z**2)
181-
182-
def unit(self):
183-
return self / self.norm()
184-
185-
186-
class Transform:
187-
def __init__(self, matrix: numpy.ndarray):
188-
self.matrix = matrix
189-
190-
def __mul__(self, other: Union[numpy.ndrray, Transform]) -> Transform:
191-
if isinstance(other, numpy.ndarray):
192-
return Transform(self.matrix @ other)
193-
elif isinstance(other, Transform):
194-
return Transform(self.matrix @ other.matrix)
195-
else:
196-
raise TypeError("Can only multiply by numpy array or Transform")
197-
198-
@staticmethod
199-
def identity() -> Transform:
200-
return Transform(numpy.eye(4))
201-
202-
@staticmethod
203-
def from_euler(order: str, angles: List[float]):
204-
matrix = numpy.identity(4)
205-
matrix[:3, :3] = Rotation.from_euler(order, angles).as_matrix()
206-
return Transform(matrix)
207-
208-
@staticmethod
209-
def rotate_around_axis(theta, axis_vector):
210-
from math import cos, sin
211-
u = axis_vector.unit()
212-
m = [
213-
[cos(theta) + u.x ** 2 * (1 - cos(theta)), u.x * u.y * (1 - cos(theta)) - u.z * sin(theta),
214-
u.x * u.z * (1 - cos(theta)) + u.y * sin(theta), 0],
215-
[u.y * u.x * (1 - cos(theta)) + u.z * sin(theta), cos(theta) + u.y ** 2 * (1 - cos(theta)),
216-
u.y * u.z * (1 - cos(theta)) - u.x * sin(theta), 0],
217-
[u.z * u.x * (1 - cos(theta)) - u.y * sin(theta), u.z * u.y * (1 - cos(theta)) + u.x * sin(theta),
218-
cos(theta) + u.z ** 2 * (1 - cos(theta)), 0],
219-
[0, 0, 0, 1]
220-
]
221-
return Transform(numpy.array(m))
222-
223-
@staticmethod
224-
def translate(*args):
225-
if len(args) == 3:
226-
return Transform._translate(*args)
227-
elif len(args) == 1:
228-
arg = args[0]
229-
if hasattr(arg, "x") and hasattr(arg, "y") and hasattr(arg, "z"):
230-
return Transform.translate(arg.x, arg.y, arg.z)
231-
raise TypeError(f"Could not create translation from {args}")
232-
233-
@staticmethod
234-
def _translate(x, y, z):
235-
m = numpy.identity(4)
236-
m[0, 3] = x
237-
m[1, 3] = y
238-
m[2, 3] = z
239-
return Transform(m)
240-
241-
def invert(self) -> Transform:
242-
return Transform(numpy.linalg.inv(self.matrix))
243-

Diff for: python/lrmate/transforms.py

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
from __future__ import annotations
2+
3+
import math
4+
5+
import numpy
6+
from dataclasses import dataclass
7+
from scipy.spatial.transform import Rotation
8+
from typing import List, Optional, Dict, Union
9+
10+
11+
@dataclass
12+
class Vector:
13+
x: float
14+
y: float
15+
z: float
16+
17+
def __add__(self, other):
18+
return Vector(self.x + other.x, self.y + other.y, self.z + other.z)
19+
20+
def __sub__(self, other):
21+
return Vector(self.x - other.x, self.y - other.y, self.z - other.z)
22+
23+
def __mul__(self, other: Union[float, Transform]) -> Vector:
24+
if isinstance(other, float):
25+
return Vector(self.x * other, self.y * other, self.z * other)
26+
elif isinstance(other, Transform):
27+
moved = other * numpy.array([self.x, self.y, self.z, 1])
28+
return Vector(moved[0], moved[1], moved[2])
29+
else:
30+
raise TypeError("Can only multiply by scalar")
31+
32+
def __truediv__(self, other: float) -> Vector:
33+
return Vector(self.x / other, self.y / other, self.z / other)
34+
35+
def norm(self):
36+
return numpy.sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2)
37+
38+
def unit(self):
39+
return self / self.norm()
40+
41+
42+
class Transform:
43+
def __init__(self, matrix: numpy.ndarray):
44+
self.matrix = matrix
45+
46+
def __mul__(self, other: Union[numpy.ndrray, Transform]) -> Transform:
47+
if isinstance(other, numpy.ndarray):
48+
return Transform(self.matrix @ other)
49+
elif isinstance(other, Transform):
50+
return Transform(self.matrix @ other.matrix)
51+
else:
52+
raise TypeError("Can only multiply by numpy array or Transform")
53+
54+
@staticmethod
55+
def identity() -> Transform:
56+
return Transform(numpy.eye(4))
57+
58+
@staticmethod
59+
def from_euler(order: str, angles: List[float]):
60+
matrix = numpy.identity(4)
61+
matrix[:3, :3] = Rotation.from_euler(order, angles).as_matrix()
62+
return Transform(matrix)
63+
64+
@staticmethod
65+
def rotate_around_axis(theta, axis_vector):
66+
from math import cos, sin
67+
u = axis_vector.unit()
68+
m = [
69+
[cos(theta) + u.x ** 2 * (1 - cos(theta)), u.x * u.y * (1 - cos(theta)) - u.z * sin(theta),
70+
u.x * u.z * (1 - cos(theta)) + u.y * sin(theta), 0],
71+
[u.y * u.x * (1 - cos(theta)) + u.z * sin(theta), cos(theta) + u.y ** 2 * (1 - cos(theta)),
72+
u.y * u.z * (1 - cos(theta)) - u.x * sin(theta), 0],
73+
[u.z * u.x * (1 - cos(theta)) - u.y * sin(theta), u.z * u.y * (1 - cos(theta)) + u.x * sin(theta),
74+
cos(theta) + u.z ** 2 * (1 - cos(theta)), 0],
75+
[0, 0, 0, 1]
76+
]
77+
return Transform(numpy.array(m))
78+
79+
@staticmethod
80+
def translate(*args):
81+
if len(args) == 3:
82+
return Transform._translate(*args)
83+
elif len(args) == 1:
84+
arg = args[0]
85+
if hasattr(arg, "x") and hasattr(arg, "y") and hasattr(arg, "z"):
86+
return Transform.translate(arg.x, arg.y, arg.z)
87+
raise TypeError(f"Could not create translation from {args}")
88+
89+
@staticmethod
90+
def _translate(x, y, z):
91+
m = numpy.identity(4)
92+
m[0, 3] = x
93+
m[1, 3] = y
94+
m[2, 3] = z
95+
return Transform(m)
96+
97+
def invert(self) -> Transform:
98+
return Transform(numpy.linalg.inv(self.matrix))
99+
100+
101+
@dataclass
102+
class XyzWpr:
103+
""" Represents a transform in Fanuc's XYZWPR format, where X, Y, and Z are in mm and W, P, and R are in degrees. """
104+
x: float
105+
y: float
106+
z: float
107+
w: float
108+
p: float
109+
r: float
110+
111+
def to_transform(self) -> Transform:
112+
yaw = Transform.rotate_around_axis(numpy.radians(self.w), Vector(0, 0, 1))
113+
pitch = Transform.rotate_around_axis(numpy.radians(self.p), Vector(0, 1, 0))
114+
roll = Transform.rotate_around_axis(numpy.radians(self.r), Vector(1, 0, 0))
115+
tilted = yaw * pitch * roll
116+
return Transform.translate(self.x, self.y, self.z) * tilted
117+
118+
@staticmethod
119+
def from_transform(transform: Transform) -> XyzWpr:
120+
# Check that transform.matrix is a 4x4 matrix
121+
if transform.matrix.shape != (4, 4):
122+
raise ValueError(f"Expected a 4x4 matrix, got {transform.matrix.shape}")
123+
124+
x = transform.matrix[0, 3]
125+
y = transform.matrix[1, 3]
126+
z = transform.matrix[2, 3]
127+
128+
# Check that the rotation matrix is orthogonal
129+
if transform.matrix[2, 0] > (1.0 - 1e-6):
130+
p = -math.pi * 0.5
131+
r = 0
132+
w = math.atan2(-transform.matrix[0, 1], -transform.matrix[0, 2])
133+
elif transform.matrix[2, 0] < (-1.0 + 1e-6):
134+
p = math.pi * 0.5
135+
r = 0
136+
w = math.atan2(transform.matrix[1, 2], transform.matrix[1, 1])
137+
else:
138+
p = math.atan2(-transform.matrix[2, 0], math.sqrt(transform.matrix[0, 0] ** 2 + transform.matrix[1, 0] ** 2))
139+
w = math.atan2(transform.matrix[1, 0], transform.matrix[0, 0])
140+
r = math.atan2(transform.matrix[2, 1], transform.matrix[2, 2])
141+
142+
return XyzWpr(x, y, z, _from_radians(w), _from_radians(p), _from_radians(r))
143+
144+
145+
def _from_radians(radians: float) -> float:
146+
return radians * 180 / math.pi

Diff for: python/requirements.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ setuptools~=65.5.1
22
numpy~=1.24.2
33
open3d~=0.17.0
44
scipy~=1.10.1
5-
urdf-parser-py~=0.0.4
5+
urdf-parser-py~=0.0.4
6+
pytest~=7.3.1

Diff for: python/tests/__init__.py

Whitespace-only changes.

Diff for: python/tests/_test_data.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
_transforms_and_xyzwpr = [
2+
([[-0.355708, 0.717742, 0.598597, 250.689172], [-0.173709, -0.680099, 0.712243, 190.09217],
3+
[0.918312, 0.149368, 0.366595, 377.021336], [0.0, 0.0, 0.0, 1.0]],
4+
[250.689172, 190.09217, 377.021336, 22.168324, -66.680543, -153.971487]),
5+
([[0.875453, 0.335633, 0.347755, 324.515443], [0.474534, -0.733383, -0.486792, -269.225543],
6+
[0.091654, 0.591185, -0.801312, 109.845856], [0.0, 0.0, 0.0, 1.0]],
7+
[324.515443, -269.225543, 109.845856, 143.581155, -5.258767, 28.459638]),
8+
([[0.829857, -0.290899, 0.476147, 480.998803], [-0.3928, -0.910637, 0.128247, -24.848409],
9+
[0.39629, -0.293458, -0.869964, 44.45118], [0.0, 0.0, 0.0, 1.0]],
10+
[480.998803, -24.848409, 44.45118, -161.359637, -23.346471, -25.329844]),
11+
([[0.622543, 0.659905, 0.420672, 427.425001], [0.286126, -0.692256, 0.662506, 317.808911],
12+
[0.728404, -0.292073, -0.619775, 185.181686], [0.0, 0.0, 0.0, 1.0]],
13+
[427.425001, 317.808911, 185.181686, -154.76754, -46.752739, 24.683898]),
14+
([[0.121273, 0.016312, 0.992485, 640.024599], [0.027579, -0.999534, 0.013058, 54.109546],
15+
[0.992236, 0.025788, -0.121667, 303.808089], [0.0, 0.0, 0.0, 1.0]],
16+
[640.024599, 54.109546, 303.808089, 168.032738, -82.855635, 12.811951]),
17+
([[-0.155532, -0.422104, 0.893106, 416.744163], [0.025165, -0.905508, -0.423583, -205.770414],
18+
[0.98751, -0.043406, 0.151457, 328.606008], [0.0, 0.0, 0.0, 1.0]],
19+
[416.744163, -205.770414, 328.606008, -15.991663, -80.93504, 170.809286]),
20+
([[-0.027822, -0.180557, 0.983171, 431.202555], [-0.281693, -0.942275, -0.181018, -21.951723],
21+
[0.959101, -0.281989, -0.024646, 228.94672], [0.0, 0.0, 0.0, 1.0]],
22+
[431.202555, -21.951723, 228.94672, -94.99493, -73.556872, -95.640661]),
23+
([[-0.7791, -0.238741, 0.579661, 243.109205], [0.547427, -0.709679, 0.443485, 126.817247],
24+
[0.305495, 0.662841, 0.683604, 442.214652], [0.0, 0.0, 0.0, 1.0]],
25+
[243.109205, 126.817247, 442.214652, 44.116508, -17.787954, 144.906558]),
26+
([[-0.338094, -0.333176, 0.880162, 342.415569], [0.15182, -0.942296, -0.298378, -7.40816],
27+
[0.928786, 0.032746, 0.369167, 346.902494], [0.0, 0.0, 0.0, 1.0]],
28+
[342.415569, -7.40816, 346.902494, 5.069014, -68.246332, 155.817742]),
29+
]

Diff for: python/tests/test_xyzwpr.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import pytest
2+
import numpy
3+
from lrmate.transforms import Transform, XyzWpr, Vector
4+
from ._test_data import _transforms_and_xyzwpr
5+
6+
7+
@pytest.mark.parametrize('matrix,xyzrpw', _transforms_and_xyzwpr)
8+
def test_xyzwpr_to_transform(matrix, xyzrpw):
9+
transform = Transform(numpy.matrix(matrix))
10+
frame = XyzWpr(*xyzrpw[:3], xyzrpw[5], xyzrpw[4], xyzrpw[3])
11+
converted = frame.to_transform()
12+
assert numpy.allclose(converted.matrix, transform.matrix, rtol=1e-4, atol=1e-5)
13+
14+
15+
@pytest.mark.parametrize('matrix,xyzrpw', _transforms_and_xyzwpr)
16+
def test_transform_to_xyzwpr(matrix, xyzrpw):
17+
transform = Transform(numpy.matrix(matrix))
18+
frame = XyzWpr(*xyzrpw[:3], xyzrpw[5], xyzrpw[4], xyzrpw[3])
19+
converted = XyzWpr.from_transform(transform)
20+
assert frame.x == pytest.approx(converted.x, abs=1e-4)
21+
assert frame.y == pytest.approx(converted.y, abs=1e-4)
22+
assert frame.z == pytest.approx(converted.z, abs=1e-4)
23+
assert frame.w == pytest.approx(converted.w, abs=1e-3)
24+
assert frame.p == pytest.approx(converted.p, abs=1e-3)
25+
assert frame.r == pytest.approx(converted.r, abs=1e-3)

0 commit comments

Comments
 (0)