ProjectiveGeometry23 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ProjectiveGeometry23/__init__.py +1 -0
- ProjectiveGeometry23/central_projection.py +249 -0
- ProjectiveGeometry23/estimation.py +71 -0
- ProjectiveGeometry23/homography.py +136 -0
- ProjectiveGeometry23/pluecker.py +161 -0
- ProjectiveGeometry23/source_detector_geometry.py +103 -0
- ProjectiveGeometry23/svg_utils.py +141 -0
- ProjectiveGeometry23/utils.py +238 -0
- projectivegeometry23-0.1.0.dist-info/METADATA +142 -0
- projectivegeometry23-0.1.0.dist-info/RECORD +13 -0
- projectivegeometry23-0.1.0.dist-info/WHEEL +5 -0
- projectivegeometry23-0.1.0.dist-info/licenses/LICENSE +201 -0
- projectivegeometry23-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# This file makes ProjectiveGeometry23 a Python package.
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Linear projection from real projective three-space to real projective two-space.
|
|
3
|
+
Representations of a pinhole camera as Projection matrix optionally with additional information of image size and pixelation.
|
|
4
|
+
|
|
5
|
+
Author: André Aichert
|
|
6
|
+
Date: June 22, 2023
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import scipy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# recommended:
|
|
14
|
+
# from rich import print
|
|
15
|
+
# np.set_printoptions(suppress=True)
|
|
16
|
+
|
|
17
|
+
class ProjectionMatrix:
|
|
18
|
+
def __init__(self, P, image_size=(400, 300), pixel_spacing=1.0):
|
|
19
|
+
"""Pinhole projection model as 3x4 projection matrix with optional
|
|
20
|
+
information of detector size and pixel spacing [mm per px]."""
|
|
21
|
+
self.P = np.array(P).reshape((3, 4))
|
|
22
|
+
self.image_size = np.array(image_size)
|
|
23
|
+
self.pixel_spacing = pixel_spacing
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def perspective_look_at(cls, eye, center=np.array([0, 0, 0]), image_size=(400, 300), fovy_rad=0.5, pixel_spacing=1.0):
|
|
28
|
+
"""Create a ProjectionMatrix that looks at a center point from and eye point given field-of-view and image size.
|
|
29
|
+
|
|
30
|
+
Extrinsic parameters as rotation matix R and translation vector t:
|
|
31
|
+
- eye: Position [X,Y,Z] of the camera (camera center)
|
|
32
|
+
- center: Point the camera is looking at (target position)
|
|
33
|
+
|
|
34
|
+
Intrinsic parameters of a pinhole camera as a 3x3 matrix K.
|
|
35
|
+
- fovy_rad: Field of view angle in radians (vertical)
|
|
36
|
+
- image_size: Width and height of the image sensor
|
|
37
|
+
|
|
38
|
+
Returns ProjectionMatrix P=K[R t] with given image_size
|
|
39
|
+
|
|
40
|
+
TODO: optional pixel_spacing should probably affect intrinsics
|
|
41
|
+
"""
|
|
42
|
+
# Up vector defining the camera's orientation
|
|
43
|
+
up=np.array([0, 1, 0])
|
|
44
|
+
|
|
45
|
+
# Intrinsics
|
|
46
|
+
w, h = image_size
|
|
47
|
+
tanfov2 = 2 * np.tan(0.5 * fovy_rad)
|
|
48
|
+
fx, fy = h / tanfov2, h / tanfov2
|
|
49
|
+
cx, cy = 0.5 * w, 0.5 * h
|
|
50
|
+
|
|
51
|
+
K = np.array([[fx, 0, cx],
|
|
52
|
+
[0, fy, cy],
|
|
53
|
+
[0, 0, 1]])
|
|
54
|
+
|
|
55
|
+
# Extrinsics
|
|
56
|
+
fwd = (center - eye) / np.linalg.norm(center - eye)
|
|
57
|
+
left = np.cross(up, fwd)
|
|
58
|
+
left /= np.linalg.norm(left)
|
|
59
|
+
new_up = np.cross(fwd, left)
|
|
60
|
+
|
|
61
|
+
R = np.array([left, new_up, -fwd])
|
|
62
|
+
t = -R.dot(eye)
|
|
63
|
+
|
|
64
|
+
# Compose projection matrix
|
|
65
|
+
P = np.zeros((3, 4))
|
|
66
|
+
P[:, :3] = K @ R
|
|
67
|
+
P[:, 3] = K @ t
|
|
68
|
+
|
|
69
|
+
return cls(P, image_size, pixel_spacing)
|
|
70
|
+
|
|
71
|
+
def to_dict(self):
|
|
72
|
+
return {
|
|
73
|
+
"P": self.P.tolist(),
|
|
74
|
+
"image_size": self.image_size.to_dict(),
|
|
75
|
+
"pixel_spacing": self.pixel_spacing
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
def to_ompl(self, with_geometry=True):
|
|
79
|
+
def f2str(f):
|
|
80
|
+
return np.format_float_positional(f, trim='-')
|
|
81
|
+
s = "; ".join([" ".join([f2str(x) for x in row]) for row in self.P])
|
|
82
|
+
if with_geometry:
|
|
83
|
+
return f'#> spacing="{self.pixel_spacing}" detector_size_px="{self.image_size}"\n[{s}]'
|
|
84
|
+
else:
|
|
85
|
+
return f'[{s}]'
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_dict(cls, data):
|
|
89
|
+
return cls(
|
|
90
|
+
data["P"],
|
|
91
|
+
np.array(data["image_size"]),
|
|
92
|
+
data["pixel_spacing"]
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def to_json(self):
|
|
96
|
+
return json.dumps(self.to_dict(), indent=2)
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_json(cls, json_str):
|
|
100
|
+
data = json.loads(json_str)
|
|
101
|
+
return cls.from_dict(data)
|
|
102
|
+
|
|
103
|
+
def __repr__(self):
|
|
104
|
+
return f"ProjectionMatrix:\n{self.P}\nImage Size: {self.image_size}\nPixel Spacing: {self.pixel_spacing}"
|
|
105
|
+
|
|
106
|
+
def principal_ray(self):
|
|
107
|
+
return self.P[2][0:3]
|
|
108
|
+
|
|
109
|
+
def normalize(self):
|
|
110
|
+
norm_m3 = np.linalg.norm(self.principal_ray())
|
|
111
|
+
detM = np.linalg.det(self.P[0:3, 0:3])
|
|
112
|
+
if detM < 0:
|
|
113
|
+
norm_m3 *= -1;
|
|
114
|
+
self.P /= norm_m3
|
|
115
|
+
|
|
116
|
+
def getCenterOfProjection(self):
|
|
117
|
+
_, _, V = np.linalg.svd(self.P)
|
|
118
|
+
C = V[-1, :4]
|
|
119
|
+
if C[3] < -1e-12 or C[3] > 1e-12:
|
|
120
|
+
C = C / C[3] # By definition: Camera centers are always positive points.
|
|
121
|
+
return C.reshape(-1, 1)
|
|
122
|
+
|
|
123
|
+
def getPrincipalPoint(self):
|
|
124
|
+
pp = self.P[:3, :3] @ self.P[2, :3].T
|
|
125
|
+
return pp / pp[2]
|
|
126
|
+
|
|
127
|
+
def getPrincipalRay(self):
|
|
128
|
+
m3 = self.P[2, :3]
|
|
129
|
+
if np.linalg.norm(m3) > 1e-12:
|
|
130
|
+
m3 = m3 / np.linalg.norm(m3)
|
|
131
|
+
return m3.reshape(-1, 1)
|
|
132
|
+
|
|
133
|
+
def getFocalLengthPx(self):
|
|
134
|
+
"""Compute the focal length in pixels (diagonal entries of K in P=K[R t] )."""
|
|
135
|
+
self.normalize()
|
|
136
|
+
|
|
137
|
+
m1 = self.P[0, :3]
|
|
138
|
+
m2 = self.P[1, :3]
|
|
139
|
+
m3 = self.P[2, :3]
|
|
140
|
+
|
|
141
|
+
U = np.cross(m3, m2) / np.linalg.norm(np.cross(m3, m2))
|
|
142
|
+
V = np.cross(m3, m1) / np.linalg.norm(np.cross(m3, m1))
|
|
143
|
+
|
|
144
|
+
focal_length_u_px = np.dot(m1, np.cross(V, m3))
|
|
145
|
+
# focal_length_v_px = np.dot(m2, np.cross(U, m3)) # should be identical
|
|
146
|
+
|
|
147
|
+
return focal_length_u_px
|
|
148
|
+
|
|
149
|
+
def getDetectorAxisDirections(self):
|
|
150
|
+
"""Compute the two three-points where the image u- and v-axes meet infinity. Scaled to world coordinates."""
|
|
151
|
+
self.normalize()
|
|
152
|
+
|
|
153
|
+
m1 = self.P[0, :3]
|
|
154
|
+
m2 = self.P[1, :3]
|
|
155
|
+
m3 = self.P[2, :3]
|
|
156
|
+
|
|
157
|
+
U = np.append(np.cross(m3, m2) / np.linalg.norm(np.cross(m3, m2)), 0)
|
|
158
|
+
V = np.append(np.cross(m3, m1) / np.linalg.norm(np.cross(m3, m1)), 0)
|
|
159
|
+
|
|
160
|
+
return U, V
|
|
161
|
+
|
|
162
|
+
def getDetectorAxisDirectionsPx(self):
|
|
163
|
+
"""Compute the two three-points where the image u- and v-axes
|
|
164
|
+
meet infinity. Scaled to pixels."""
|
|
165
|
+
U, V = getDetectorAxisDirections()
|
|
166
|
+
return U * self.pixel_spacing, V * self.pixel_spacing
|
|
167
|
+
|
|
168
|
+
def getDetectorPlane(self, imageVPointsUp=False):
|
|
169
|
+
""" Decomposes the projection matrix to compute the equation of the
|
|
170
|
+
image/detector plane. Assumes rectangular pixels. This is identical to
|
|
171
|
+
the principal place shifted by the focal length. For left-handed
|
|
172
|
+
coordinate systems, set imageVPointsUp to True.
|
|
173
|
+
Returns the image detector plane in Hessian normal form and a boolean
|
|
174
|
+
indicating whether imageVPointsUp appears to be set correctly. """
|
|
175
|
+
# TODO: alternative (that won't know when handedness is off)
|
|
176
|
+
# focal_length_u_mm = self.getFocalLength() * self.pixel_spacing
|
|
177
|
+
K, R, t, appears_flipped = self.decomposition(imageVPointsUp)
|
|
178
|
+
focal_length_u_mm = K[0, 0] * self.pixel_spacing
|
|
179
|
+
principal_plane = self.P[2, :4].copy()
|
|
180
|
+
principal_plane /= np.linalg.norm(principal_plane[:3])
|
|
181
|
+
principal_plane[3] -= focal_length_u_mm
|
|
182
|
+
return principal_plane, appears_flipped
|
|
183
|
+
|
|
184
|
+
def decomposition(self, imageVPointsUp = False):
|
|
185
|
+
"""Decompose Projection Matrix into K[R|t] using RQ-decomposition.
|
|
186
|
+
Returns K, R, t and a boolean indicating if R is left-handed.
|
|
187
|
+
For right-handed world coordinates implies imageVPointsUp is wrong."""
|
|
188
|
+
# Compute RQ decomposition of leftmost 3x3 sub-matrix of P
|
|
189
|
+
R, Q = scipy.linalg.rq(self.P[:3, :3].copy())
|
|
190
|
+
K = R
|
|
191
|
+
R = Q
|
|
192
|
+
# make diagonal of K positive
|
|
193
|
+
S = np.eye(3)
|
|
194
|
+
if K[0, 0] < 0:
|
|
195
|
+
S[0, 0] = -1
|
|
196
|
+
if K[1, 1] < 0:
|
|
197
|
+
S[1, 1] = -1
|
|
198
|
+
if imageVPointsUp:
|
|
199
|
+
S[1, 1] *= -1
|
|
200
|
+
if K[2, 2] < 0:
|
|
201
|
+
S[2, 2] = -1
|
|
202
|
+
K = K @ S
|
|
203
|
+
R = S @ R
|
|
204
|
+
# Force zero elements in K
|
|
205
|
+
K[1, 0] = K[2, 0] = K[2, 1] = 0
|
|
206
|
+
# Scale
|
|
207
|
+
K *= 1.0 / K[2, 2]
|
|
208
|
+
t = np.linalg.solve(K, self.P[:3, 3])
|
|
209
|
+
# EXPLANATION of appears_flipped:
|
|
210
|
+
# In oriented projective geometry, for a visible point x=P*X the
|
|
211
|
+
# homogeneous coordinate will be positive. A negative homogeneous
|
|
212
|
+
# coordinate implies "behind the camera". You can thus change the
|
|
213
|
+
# direction in which a camera is facing by a multiplication of the
|
|
214
|
+
# projection matrix with -1.
|
|
215
|
+
# This is relevant for the decomposition, because in practice, the
|
|
216
|
+
# coordinate system can be left-handed. In this case, the image
|
|
217
|
+
# "rotation" matrix has negative determinant. This may actually be the
|
|
218
|
+
# case, e.g. for the way pixels are stored in a BMP file, some window
|
|
219
|
+
# coordinates or Siemens 'Leonardo' style raw images. In most cases,
|
|
220
|
+
# though, that means you got the oriantation wrong.
|
|
221
|
+
# You can check using sourceDetectorGeometry. Only if your pixel
|
|
222
|
+
# spacing and imageVPointsUp values are correct, will the image plane
|
|
223
|
+
# and detector origin be on the opposite side of the object w.r.t.
|
|
224
|
+
# source position (as it should be... duh).
|
|
225
|
+
appears_flipped = np.linalg.det(R) > 0
|
|
226
|
+
return K, R, t, appears_flipped
|
|
227
|
+
|
|
228
|
+
def pseudoinverse(self):
|
|
229
|
+
return np.linalg.pinv(self.P)
|
|
230
|
+
|
|
231
|
+
def backproject(self, x):
|
|
232
|
+
"""Compute Plücker Coordinates of a backprojection for a 2D image point x in homogeneous coordinates.
|
|
233
|
+
if x is a list, make sure to convert o a column vector np.array(x).reshape(-1, 1)."""
|
|
234
|
+
Pinv = self.pseudoinverse()
|
|
235
|
+
return Pinv @ x
|
|
236
|
+
|
|
237
|
+
def computeFundamentalMatrix(self, P1):
|
|
238
|
+
"""Compute fundamental matrix from two projection matrices. Pseudoinverse-based implementation"""
|
|
239
|
+
if isinstance(P1, ProjectionMatrix):
|
|
240
|
+
P1 = P1.P
|
|
241
|
+
C0 = self.getCenterOfProjection()
|
|
242
|
+
e1 = P1 @ C0
|
|
243
|
+
e1 = e1.flatten() / np.linalg.norm(e1)
|
|
244
|
+
e1x = np.array([[ 0, +e1[2], -e1[1]],
|
|
245
|
+
[-e1[2], 0, +e1[0]],
|
|
246
|
+
[+e1[1], -e1[0], 0]])
|
|
247
|
+
P0plus = self.pseudoinverse()
|
|
248
|
+
|
|
249
|
+
return np.dot(np.dot(e1x, P1), P0plus)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Direct Linear Transformation (DLT)
|
|
3
|
+
Author: André Aichert
|
|
4
|
+
Date: June 22, 2023
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
def dlt_normalization(p2d, p3d, matches):
|
|
11
|
+
"""Normalization of 2D and 3D point clouds. Input assumed to have homogeneous coordinate 1.
|
|
12
|
+
Purpose: De-mean and scale to +/- sqtr(2), sqrt(3) respectively. """
|
|
13
|
+
# Compute mean
|
|
14
|
+
mean2d = np.array([0, 0])
|
|
15
|
+
mean3d = np.array([0, 0, 0])
|
|
16
|
+
for i, j in matches:
|
|
17
|
+
mean2d += p2d[i][:2]
|
|
18
|
+
mean3d += p3d[j][:3]
|
|
19
|
+
mean2d /= len(match)
|
|
20
|
+
mean3d /= len(match)
|
|
21
|
+
|
|
22
|
+
# Compute size
|
|
23
|
+
s2d = 0
|
|
24
|
+
s3d = 0
|
|
25
|
+
for i, j in matches:
|
|
26
|
+
s2d += np.linalg.norm(p2d[i][:2] - mean2d)
|
|
27
|
+
s3d += np.linalg.norm(p3d[j][:3] - mean3d)
|
|
28
|
+
s2d *= np.sqrt(2) / len(match)
|
|
29
|
+
s3d *= np.sqrt(3) / len(match)
|
|
30
|
+
|
|
31
|
+
# Compose normalization matrices
|
|
32
|
+
normalization_2d = np.matmul(scale(s2d, s2d), translation(-mean2d[0], -mean2d[1]))
|
|
33
|
+
normalization_3d = np.matmul(scale(s3d, s3d, s3d), translation(-mean3d[0], -mean3d[1], -mean3d[2]))
|
|
34
|
+
|
|
35
|
+
return normalization_2d, normalization_3d
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def dlt(x, X, match=None):
|
|
39
|
+
"""Direct Linear Transformation (DLT) for projection matrices."""
|
|
40
|
+
# Optional match parameter
|
|
41
|
+
if not match and len(X) == len(x):
|
|
42
|
+
match = {i: i for i in range(len(X))}
|
|
43
|
+
|
|
44
|
+
# Check for insufficient data
|
|
45
|
+
if len(match) < 6:
|
|
46
|
+
return np.zeros((3, 4))
|
|
47
|
+
|
|
48
|
+
# Normalization of input data
|
|
49
|
+
N_px, N_mm = dlt_normalization(x, X, match)
|
|
50
|
+
|
|
51
|
+
# Build homogeneous system matrix from point matches
|
|
52
|
+
A = np.zeros((2 * len(match), 12))
|
|
53
|
+
for k, (key, value) in enumerate(match.items()):
|
|
54
|
+
# Normalize input points
|
|
55
|
+
x_norm = np.dot(N_px, x[key])
|
|
56
|
+
X_norm = np.dot(N_mm, X[value])
|
|
57
|
+
# Write two rows in A (we get two independent equations from one point match)
|
|
58
|
+
A[2 * k, 4:8] = x_norm[2] * X_norm
|
|
59
|
+
A[2 * k + 1, 0:4] = -x_norm[2] * X_norm
|
|
60
|
+
A[2 * k, 8:12] = -x_norm[1] * X_norm
|
|
61
|
+
A[2 * k + 1, 8:12] = x_norm[0] * X_norm
|
|
62
|
+
|
|
63
|
+
# Solve and reshape
|
|
64
|
+
_, _, V = np.linalg.svd(A)
|
|
65
|
+
p = V[-1] / V[-1, -1]
|
|
66
|
+
P_norm = p.reshape((3, 4))
|
|
67
|
+
|
|
68
|
+
# Denormalize
|
|
69
|
+
P = np.dot(np.dot(np.linalg.inv(N_px), P_norm), N_mm)
|
|
70
|
+
P /= np.linalg.norm(P[:, :3], axis=0)
|
|
71
|
+
return P
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Useful homographies on real projective two- and three-space.
|
|
3
|
+
Author: André Aichert
|
|
4
|
+
Date: June 22, 2023
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def translation2d(t):
|
|
11
|
+
"""Homogeneous 2D translation."""
|
|
12
|
+
T = np.array([
|
|
13
|
+
[1, 0, t[0]],
|
|
14
|
+
[0, 1, t[1]],
|
|
15
|
+
[0, 0, 1]
|
|
16
|
+
])
|
|
17
|
+
return T
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def scale2d(s):
|
|
21
|
+
"""Homogeneous 2D scaling."""
|
|
22
|
+
if isinstance(s, (int, float)):
|
|
23
|
+
s = [s,s]
|
|
24
|
+
S = np.array([
|
|
25
|
+
[s[0], 0, 0],
|
|
26
|
+
[0, s[1], 0],
|
|
27
|
+
[0, 0, 1]
|
|
28
|
+
])
|
|
29
|
+
return S
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def rotation2d(alpha):
|
|
33
|
+
ca = np.cos(alpha)
|
|
34
|
+
sa = np.sin(alpha)
|
|
35
|
+
R = np.array([
|
|
36
|
+
[ca, -sa, 0],
|
|
37
|
+
[sa, ca, 0],
|
|
38
|
+
[ 0, 0, 1]
|
|
39
|
+
])
|
|
40
|
+
return R
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def rigid_transform(alpha, tu, tv):
|
|
44
|
+
"""Homogeneous 2D rigid transformation."""
|
|
45
|
+
ca = np.cos(alpha)
|
|
46
|
+
sa = np.sin(alpha)
|
|
47
|
+
R = np.array([
|
|
48
|
+
[ca, -sa, tu],
|
|
49
|
+
[sa, ca, tv],
|
|
50
|
+
[ 0, 0, 1]
|
|
51
|
+
])
|
|
52
|
+
return R
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def translation(t):
|
|
56
|
+
"""Homogeneous 3D translation."""
|
|
57
|
+
T = np.array([
|
|
58
|
+
[1, 0, 0, t[0]],
|
|
59
|
+
[0, 1, 0, t[1]],
|
|
60
|
+
[0, 0, 1, t[2]],
|
|
61
|
+
[0, 0, 0, 1]
|
|
62
|
+
])
|
|
63
|
+
return T
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def scale(s):
|
|
67
|
+
"""Homogeneous 3D scaling."""
|
|
68
|
+
if isinstance(s, (int, float)):
|
|
69
|
+
s = [s,s,s]
|
|
70
|
+
S = np.array([
|
|
71
|
+
[s[0], 0, 0, 0],
|
|
72
|
+
[0, s[1], 0, 0],
|
|
73
|
+
[0, 0, s[2], 0],
|
|
74
|
+
[0, 0, 0, 1]
|
|
75
|
+
])
|
|
76
|
+
return S
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def rotation_x(alpha):
|
|
80
|
+
"""Homogeneous rotation about X-axis."""
|
|
81
|
+
ca = np.cos(alpha)
|
|
82
|
+
sa = np.sin(alpha)
|
|
83
|
+
R = np.array([
|
|
84
|
+
[1, 0, 0, 0],
|
|
85
|
+
[0, ca, -sa, 0],
|
|
86
|
+
[0, sa, ca, 0],
|
|
87
|
+
[0, 0, 0, 1]
|
|
88
|
+
])
|
|
89
|
+
return R
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def rotation_y(alpha):
|
|
93
|
+
"""Homogeneous rotation about Y-axis."""
|
|
94
|
+
ca = np.cos(alpha)
|
|
95
|
+
sa = np.sin(alpha)
|
|
96
|
+
R = np.array([
|
|
97
|
+
[ca, 0, sa, 0],
|
|
98
|
+
[ 0, 1, 0, 0],
|
|
99
|
+
[-sa, 0, ca, 0],
|
|
100
|
+
[ 0, 0, 0, 1]
|
|
101
|
+
])
|
|
102
|
+
return R
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def rotation_z(alpha):
|
|
106
|
+
"""Homogeneous rotation about Z-axis."""
|
|
107
|
+
ca = np.cos(alpha)
|
|
108
|
+
sa = np.sin(alpha)
|
|
109
|
+
R = np.array([
|
|
110
|
+
[ca, -sa, 0, 0],
|
|
111
|
+
[sa, ca, 0, 0],
|
|
112
|
+
[ 0, 0, 1, 0],
|
|
113
|
+
[ 0, 0, 0, 1]
|
|
114
|
+
])
|
|
115
|
+
return R
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def euler_to_rotation_matrix(euler_angles):
|
|
119
|
+
"""Rotation from Euler angles. Assumes input is a 3-vector containing the Euler angles."""
|
|
120
|
+
rotation_x_matrix = rotation_x(euler_angles[0])
|
|
121
|
+
rotation_y_matrix = rotation_y(euler_angles[1])
|
|
122
|
+
rotation_z_matrix = rotation_z(euler_angles[2])
|
|
123
|
+
|
|
124
|
+
R = rotation_x_matrix @ rotation_y_matrix @ rotation_z_matrix
|
|
125
|
+
return R
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def euler_from_rotation_matrix(R):
|
|
129
|
+
"""Euler angles from rotation matrix.
|
|
130
|
+
Assumes the top-left 3x3 submatrix of R is in fact an orthogonal matrix."""
|
|
131
|
+
ry = np.arcsin(R[0, 2])
|
|
132
|
+
rz = np.arccos(R[0, 0] / np.cos(ry))
|
|
133
|
+
rx = np.arccos(R[2, 2] / np.cos(ry))
|
|
134
|
+
euler_angles = np.array([rx, ry, rz])
|
|
135
|
+
return euler_angles
|
|
136
|
+
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Representation of lines in 3D space in Plücker coordinates.
|
|
3
|
+
Author: André Aichert
|
|
4
|
+
Date: June 22, 2023
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from .utils import dot, cvec, append
|
|
9
|
+
from .utils import homogenize, dehomogenize, RP3Point
|
|
10
|
+
from scipy.optimize import minimize
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def join_points(A, B):
|
|
14
|
+
"""Join two points to form a 3D line."""
|
|
15
|
+
A = A.flatten()
|
|
16
|
+
B = B.flatten()
|
|
17
|
+
return cvec([
|
|
18
|
+
A[0] * B[1] - A[1] * B[0],
|
|
19
|
+
A[0] * B[2] - A[2] * B[0],
|
|
20
|
+
A[0] * B[3] - A[3] * B[0],
|
|
21
|
+
A[1] * B[2] - A[2] * B[1],
|
|
22
|
+
A[1] * B[3] - A[3] * B[1],
|
|
23
|
+
A[2] * B[3] - A[3] * B[2]
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def meet_planes(P, Q):
|
|
28
|
+
"""Intersect two planes to form a 3D line."""
|
|
29
|
+
P = P.flatten()
|
|
30
|
+
Q = Q.flatten()
|
|
31
|
+
return cvec([
|
|
32
|
+
P[2] * Q[3] - P[3] * Q[2],
|
|
33
|
+
P[3] * Q[1] - P[1] * Q[3],
|
|
34
|
+
P[1] * Q[2] - P[2] * Q[1],
|
|
35
|
+
P[0] * Q[3] - P[3] * Q[0],
|
|
36
|
+
P[2] * Q[0] - P[0] * Q[2],
|
|
37
|
+
P[0] * Q[1] - P[1] * Q[0]
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def join(L, X):
|
|
42
|
+
"""Join a line L and a point X to form a plane."""
|
|
43
|
+
L = L.flatten()
|
|
44
|
+
X = X.flatten()
|
|
45
|
+
return cvec([
|
|
46
|
+
+ X[1] * L[5] - X[2] * L[4] + X[3] * L[3], # noqa
|
|
47
|
+
- X[0] * L[5] + X[2] * L[2] - X[3] * L[1], # noqa
|
|
48
|
+
+ X[0] * L[4] - X[1] * L[2] + X[3] * L[0], # noqa
|
|
49
|
+
- X[0] * L[3] + X[1] * L[1] - X[2] * L[0] # noqa
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def meet(L, E):
|
|
54
|
+
"""Intersect a line L and a plane E to form a point."""
|
|
55
|
+
L = L.flatten()
|
|
56
|
+
E = E.flatten()
|
|
57
|
+
return cvec([
|
|
58
|
+
- E[1] * L[0] - E[2] * L[1] - E[3] * L[2], # noqa
|
|
59
|
+
+ E[0] * L[0] - E[2] * L[3] - E[3] * L[4], # noqa
|
|
60
|
+
+ E[0] * L[1] + E[1] * L[3] - E[3] * L[5], # noqa
|
|
61
|
+
+ E[0] * L[2] + E[1] * L[4] + E[2] * L[5] # noqa
|
|
62
|
+
])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def direction(L):
|
|
66
|
+
"""Direction of a 3D line in Plücker coordinates.
|
|
67
|
+
Note that for dual Plücker coordinates, the functions direction and moment will be swapped."""
|
|
68
|
+
return np.array([-L[2][0], -L[4][0], -L[5][0]]).reshape(-1, 1)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def moment(L):
|
|
72
|
+
"""Moment of a 3D line in Plücker coordinates.
|
|
73
|
+
Note that for dual Plücker coordinates, the functions direction and moment will be swapped."""
|
|
74
|
+
return np.array([L[3][0], -L[1][0], L[0][0]]).reshape(-1, 1)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def closest_point_to_origin(L):
|
|
78
|
+
"""Closest point on line L to the origin."""
|
|
79
|
+
L = L.flatten()
|
|
80
|
+
return np.array([
|
|
81
|
+
L[4] * L[0] + L[1] * L[5], # noqa
|
|
82
|
+
-L[0] * L[2] + L[3] * L[5], # noqa
|
|
83
|
+
-L[1] * L[2] - L[3] * L[4], # noqa
|
|
84
|
+
-L[2] * L[2] - L[4] * L[4] - L[5] * L[5] # noqa
|
|
85
|
+
]).reshape(-1, 1)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def closest_to_point(L, X):
|
|
89
|
+
"""Compute the closest point on the line L to a given point X."""
|
|
90
|
+
d = direction(L)
|
|
91
|
+
plane_through_X_orthogonal_to_L = RP3Point(d[0][0], d[1][0], d[2][0], -dot(d, dehomogenize(X)))
|
|
92
|
+
closest_point_to_X_on_L = meet(L, plane_through_X_orthogonal_to_L)
|
|
93
|
+
return closest_point_to_X_on_L
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def distance_to_point(L, X):
|
|
97
|
+
"""Compute the distance of a line L to a given point X."""
|
|
98
|
+
I = closest_to_point(L, X)
|
|
99
|
+
return np.linalg.norm(dehomogenize(I) - dehomogenize(X))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def point_closest_to_lines(lines, eps=1e-9):
|
|
103
|
+
"""Find point with minimal distance to a line bundle and return (distance, point).
|
|
104
|
+
Returns intersection of two or more lines, if they do intersect."""
|
|
105
|
+
def mean_distance(Xe):
|
|
106
|
+
"""Function to be minimized. Takes 3D *euclidian* coordinates."""
|
|
107
|
+
return np.sum([distance_to_point(L, RP3Point(Xe[0], Xe[1] ,Xe[2], 1)) for L in lines]) / len(lines)
|
|
108
|
+
result = minimize(mean_distance, [0, 0, 0], tol=eps)
|
|
109
|
+
return result.fun, homogenize(result.x)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def distance_to_origin(L):
|
|
113
|
+
"""Distance of a line to the origin."""
|
|
114
|
+
m = moment(L)
|
|
115
|
+
d = direction(L)
|
|
116
|
+
return np.linalg.norm(m) / np.linalg.norm(d)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def matrixDual(L):
|
|
120
|
+
"""Anti-symmetric matrix for the join operation using dual Plücker coordinates."""
|
|
121
|
+
L = L.flatten()
|
|
122
|
+
B = np.array([
|
|
123
|
+
[ 0, L[5], -L[4], L[3]], # noqa
|
|
124
|
+
[-L[5], 0, L[2], -L[1]], # noqa
|
|
125
|
+
[ L[4], -L[2], 0, L[0]], # noqa
|
|
126
|
+
[-L[3], L[1], -L[0], 0] # noqa
|
|
127
|
+
])
|
|
128
|
+
return B
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def matrix(L):
|
|
132
|
+
"""Anti-symmetric matrix for the meet operation using Plücker coordinates."""
|
|
133
|
+
L = L.flatten()
|
|
134
|
+
B = np.array([
|
|
135
|
+
[ 0, -L[0], -L[1], -L[2]], # noqa
|
|
136
|
+
[L[0], 0, -L[3], -L[4]], # noqa
|
|
137
|
+
[L[1], L[3], 0, -L[5]], # noqa
|
|
138
|
+
[L[2], L[4], L[5], 0] # noqa
|
|
139
|
+
])
|
|
140
|
+
return B
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def projection_matrix(P):
|
|
144
|
+
"""Compute Sturm-style projection matrix for central projection of Plücker lines. Projection from Plücker coordinates directly to 2D lines written in a single 3x6 matrix. P is a standard 3x4 projection matrix."""
|
|
145
|
+
PL = np.array([
|
|
146
|
+
[P[1, 0]*P[2, 1]-P[1, 1]*P[2, 0], P[1, 0]*P[2, 2]-P[1, 2]*P[2, 0], P[1, 0]*P[2, 3]-P[1, 3]*P[2, 0], P[1, 1]*P[2, 2]-P[1, 2]*P[2, 1], P[1, 1]*P[2, 3]-P[1, 3]*P[2, 1], P[1, 2]*P[2, 3]-P[1, 3]*P[2, 2]], # noqa
|
|
147
|
+
[P[0, 1]*P[2, 0]-P[0, 0]*P[2, 1], -P[0, 0]*P[2, 2]+P[0, 2]*P[2, 0], -P[0, 0]*P[2, 3]+P[0, 3]*P[2, 0], -P[0, 1]*P[2, 2]+P[0, 2]*P[2, 1], -P[0, 1]*P[2, 3]+P[0, 3]*P[2, 1], -P[0, 2]*P[2, 3]+P[0, 3]*P[2, 2]], # noqa
|
|
148
|
+
[P[0, 0]*P[1, 1]-P[0, 1]*P[1, 0], P[0, 0]*P[1, 2]-P[0, 2]*P[1, 0], P[0, 0]*P[1, 3]-P[0, 3]*P[1, 0], P[0, 1]*P[1, 2]-P[0, 2]*P[1, 1], P[0, 1]*P[1, 3]-P[0, 3]*P[1, 1], P[0, 2]*P[1, 3]-P[0, 3]*P[1, 2]] # noqa
|
|
149
|
+
])
|
|
150
|
+
return PL
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def project(L, P):
|
|
154
|
+
"""Directly project 3D line in Plücker coordinates to 2D line.
|
|
155
|
+
In Python, it may be faster to just do np.dot(projection_matrix(P), L)"""
|
|
156
|
+
line = np.array([
|
|
157
|
+
L[0]*(P[1, 0]*P[2, 1]-P[1, 1]*P[2, 0]) + L[1]*( P[1, 0]*P[2, 2]-P[1, 2]*P[2, 0]) + L[2]*( P[1, 0]*P[2, 3]-P[1, 3]*P[2, 0]) + L[3]*( P[1, 1]*P[2, 2]-P[1, 2]*P[2, 1]) + L[4]*( P[1, 1]*P[2, 3]-P[1, 3]*P[2, 1]) + L[5]*( P[1, 2]*P[2, 3]-P[1, 3]*P[2, 2]), # noqa
|
|
158
|
+
L[0]*(P[0, 1]*P[2, 0]-P[0, 0]*P[2, 1]) + L[1]*(-P[0, 0]*P[2, 2]+P[0, 2]*P[2, 0]) + L[2]*(-P[0, 0]*P[2, 3]+P[0, 3]*P[2, 0]) + L[3]*(-P[0, 1]*P[2, 2]+P[0, 2]*P[2, 1]) + L[4]*(-P[0, 1]*P[2, 3]+P[0, 3]*P[2, 1]) + L[5]*(-P[0, 2]*P[2, 3]+P[0, 3]*P[2, 2]), # noqa
|
|
159
|
+
L[0]*(P[0, 0]*P[1, 1]-P[0, 1]*P[1, 0]) + L[1]*( P[0, 0]*P[1, 2]-P[0, 2]*P[1, 0]) + L[2]*( P[0, 0]*P[1, 3]-P[0, 3]*P[1, 0]) + L[3]*( P[0, 1]*P[1, 2]-P[0, 2]*P[1, 1]) + L[4]*( P[0, 1]*P[1, 3]-P[0, 3]*P[1, 1]) + L[5]*( P[0, 2]*P[1, 3]-P[0, 3]*P[1, 2]) # noqa
|
|
160
|
+
])
|
|
161
|
+
return line
|