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.
@@ -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