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,103 @@
1
+ """
2
+ Computations on the detector in pixels and 3D millimeters of source-detector systems such as X-ray.
3
+
4
+ Author: André Aichert
5
+ Date: June 22, 2023
6
+ """
7
+
8
+ import numpy as np
9
+ from numpy.linalg import norm
10
+ from .central_projection import ProjectionMatrix
11
+ from .utils import cvec, append, dehomogenize
12
+
13
+ class SourceDetectorGeometry:
14
+ def __init__(self, projection: ProjectionMatrix):
15
+ """From a 3x4 projection matrix and pixel spacing, compute physical location of detector.
16
+ This allows for a full visualization of a source-detector geometry as a pyramid or frustum.
17
+ In case you have a left-handed coordinate frame, use negative pixel spacing to swap the
18
+ viewing direction of the system (i.e. negate pixel spacing if projection looks backwards."""
19
+ projection.normalize()
20
+
21
+ C = projection.getCenterOfProjection()
22
+
23
+ m1 = projection.P[0, :3]
24
+ m2 = projection.P[1, :3]
25
+ m3 = projection.P[2, :3]
26
+
27
+ U = np.append(np.cross(m3, m2), 0)
28
+ V = np.append(np.cross(m3, m1), 0)
29
+
30
+ U *= projection.pixel_spacing / norm(U)
31
+ V *= projection.pixel_spacing / norm(V)
32
+
33
+ principal_plane = projection.P[2, :4]
34
+
35
+ # Note how this conversion is symmetric for U and V.
36
+ V_dir = V[:3] / projection.pixel_spacing
37
+ f = np.dot(m1, np.cross(V_dir, m3))
38
+
39
+ # However, the results will be the same ONLY in the case of rectangular pixels.
40
+ # Assumption of rectangular pixels for a digital detector is pretty safe though.
41
+ image_plane = principal_plane.copy()
42
+ image_plane[3] -= f * projection.pixel_spacing
43
+
44
+ # negative pixel spacings support flipping detector axes (left handed systems)
45
+ # Note how mulpiplication of a projection matrix with -1 inverts viewing direction.
46
+ # if projection.pixel_spacing < 0:
47
+ # V *= -1
48
+
49
+ central_projection = SourceDetectorGeometry.centralProjectionToPlane(C, image_plane)
50
+
51
+ source_detector_distance = np.dot(image_plane, C)[0]
52
+
53
+ principal_point_3d = cvec(C) - append(m3, 0) * source_detector_distance
54
+
55
+ principal_point = np.dot(projection.P, principal_point_3d)
56
+ pp = principal_point / principal_point[2]
57
+
58
+ # This is the corner of the detector where the pixel origin is located
59
+ detector_origin = principal_point_3d - cvec(U) * pp[0] - cvec(V) * pp[1]
60
+
61
+ # things that fully define the source-detector geometry:
62
+ self.source_position = C
63
+ self.detector_origin = detector_origin
64
+ self.axis_direction_Upx = U
65
+ self.axis_direction_Vpx = V
66
+ # plus some useful extras
67
+ self.image_plane = image_plane
68
+ self.principal_point_3d = principal_point_3d
69
+ self.source_detector_distance = source_detector_distance
70
+ # and a projection matrix in 3D world coordinates (3D points -> 3D points on the detector).
71
+ self.central_projection_3d = central_projection
72
+
73
+ def __repr__(self):
74
+ return "\n ".join([
75
+ "SourceDetectorGeometry:",
76
+ f"Source Position: {self.source_position.flatten()}",
77
+ f"Source Detector Distance: {self.source_detector_distance}",
78
+ f"Detector Origin: {self.detector_origin[:,0].tolist()}",
79
+ f"Principal Point 3D: {self.principal_point_3d[:,0].tolist()}",
80
+ f"Axis Orientation:\n U={self.axis_direction_Upx}\n V={self.axis_direction_Vpx}"
81
+ ])
82
+
83
+
84
+ @classmethod
85
+ def centralProjectionToPlane(cls, C, E):
86
+ """A mapping T from a 3D point C to a plane E via central projection from C.
87
+ Mapping is according to T*X=meet(join(C,X),E) written in matrix form."""
88
+ C = C.flatten()
89
+ E = E.flatten()
90
+ T = [[+ C[1]*E[1] + C[2]*E[2] + C[3]*E[3] , - C[0]*E[1] , - C[0]*E[2] , - C[0]*E[3] ], # noqa
91
+ [- C[1]*E[0] , + C[0]*E[0] + C[2]*E[2] + C[3]*E[3] , - C[1]*E[2] , - C[1]*E[3] ], # noqa
92
+ [- C[2]*E[0] , - C[2]*E[1] , + C[0]*E[0] + C[3]*E[3] + C[1]*E[1] , - C[2]*E[3] ], # noqa
93
+ [- C[3]*E[0] , - C[3]*E[1] , - C[3]*E[2] , + C[0]*E[0] + C[1]*E[1] + C[2]*E[2] ]] # noqa
94
+ return T
95
+
96
+ def detectorPixelIn3Dmm(self, u, v):
97
+ """ Compute the 3D location of a pixel (u, v) in world coordinates (mm) . See also: ProjectionMatrix.sourceDetectorGeometry()
98
+ This function mostly serves as documentation for how to interpret the source detector geometry."""
99
+ return self.detector_origin + self.axis_direction_Upx * u + self.axis_direction_Vpx * v
100
+
101
+ def projectToDetector3Dmm(self, X):
102
+ return self.central_projection_3d @ X
103
+
@@ -0,0 +1,141 @@
1
+ """
2
+ Interactive drawing of points, lines in 2d and 3D, as well as vidualizing X-ray source-detector geometries.
3
+
4
+ Usage:
5
+ !pip install svg_vis
6
+
7
+ from svg.Jupyter import CanvasWithOverlay
8
+ from pg.homography import rotation_x, rotation_z, scale
9
+ from svg.Renderer import RenderSVG
10
+
11
+ target = ProjectionMatrix([...], detector_size_px, spacing)
12
+
13
+ vis = CanvasWithOverlay(target.image_size[0], target.image_size[1])
14
+
15
+ # World transformation
16
+ ax, az = 0.5, 0.5
17
+ s = 0.2
18
+
19
+ def handle_draw(vis):
20
+ global ax
21
+ global az
22
+ x,y = vis.mouse_state.pos()
23
+ if vis.mouse_state.clicked:
24
+ az += vis.mouse_state.dx * 0.01
25
+ ax += vis.mouse_state.dy * 0.01
26
+
27
+ svg = RenderSVG((vis.w, vis.h))
28
+
29
+ svg.add(svg_world_geometry)
30
+ svg.add(svg_source_detector, projection=target,
31
+ draw_on_detector=svg_world_geometry,
32
+ label_source='C0', label_detector='I0(u,v)')
33
+
34
+ T = scale(s) @ rotation_x(ax) @ rotation_z(az)
35
+ raw_svg_code = svg.render(P=target.P@T)
36
+ vis.html_overlay.value = raw_svg_code
37
+
38
+ vis.handle_draw = handle_draw
39
+
40
+ vis.display()
41
+
42
+ Author: André Aichert
43
+ Date: Dec 11th, 2023
44
+ """
45
+
46
+
47
+ from svg_snip.Composer import Composer
48
+ import svg_snip.Elements as e2d
49
+ import svg_snip.Elements3D as e3d
50
+
51
+ import ProjectiveGeometry23.utils as pgu
52
+ from ProjectiveGeometry23 import pluecker
53
+
54
+ from ProjectiveGeometry23.central_projection import ProjectionMatrix
55
+ from ProjectiveGeometry23.source_detector_geometry import SourceDetectorGeometry
56
+
57
+
58
+ def svg_coordinate_frame(P, size=100, **kwargs):
59
+ """Draw a coordinate system of default size 100."""
60
+ el = [
61
+ '<g>',
62
+ # Coordinate frame
63
+ e3d.line(P=P, X1=[0,0,0,1], X2=[size,0,0,1], stroke='red', **kwargs),
64
+ e3d.line(P=P, X1=[0,0,0,1], X2=[0,size,0,1], stroke='green', **kwargs),
65
+ e3d.line(P=P, X1=[0,0,0,1], X2=[0,0,size,1], stroke='blue', **kwargs)
66
+ ]
67
+ return '\n '.join(el) + '\n</g>\n'
68
+
69
+
70
+ def svg_world_geometry(P, **kwargs):
71
+ """Draw a coordinate system of size 100 and a wire cube
72
+ of the same size centered in the origin."""
73
+ el = [
74
+ '<g>',
75
+ # Coordinate frame
76
+ e3d.line(P=P, X1=[0,0,0,1], X2=[100,0,0,1], stroke='red', **kwargs),
77
+ e3d.line(P=P, X1=[0,0,0,1], X2=[0,100,0,1], stroke='green', **kwargs),
78
+ e3d.line(P=P, X1=[0,0,0,1], X2=[0,0,100,1], stroke='blue', **kwargs),
79
+ # a cube with size 100
80
+ e3d.wire_cube(P=P, min=[-50,-50,-50], max=[50,50,50], stroke='black', **kwargs)
81
+ ]
82
+ return '\n '.join(el) + '\n</g>\n'
83
+
84
+
85
+ def svg_source_detector(P, projection: ProjectionMatrix, draw_on_detector=None, **kwargs):
86
+ """Draw X-ray source-detector geometry.
87
+ Define draw_on_detector as any SVG drawing function to project
88
+ additional 3D geometry to the detector plane."""
89
+ sdg = SourceDetectorGeometry(projection)
90
+ C = pgu.cvec(sdg.source_position)
91
+ O = sdg.detector_origin
92
+ U = pgu.cvec(sdg.axis_direction_Upx) * projection.image_size[0]
93
+ V = pgu.cvec(sdg.axis_direction_Vpx) * projection.image_size[1]
94
+
95
+ el = [
96
+ '<g>\n',
97
+ # Source position
98
+ e3d.point(P=P, X=C, r=1, fill="black", **kwargs),
99
+ # Detector frame
100
+ e3d.polygon(P=P, Xs=[O, O+U, O+V+U ,O+V],
101
+ fill="#00000020", stroke="#00000040", **kwargs),
102
+ e3d.line(P=P, X1=O, X2=O + U, stroke="magenta", **kwargs),
103
+ e3d.line(P=P, X1=O, X2=O + V, stroke="cyan", **kwargs),
104
+ # Frustum
105
+ e3d.line(P=P, X1=C, X2=O, stroke="#00000020", **kwargs),
106
+ e3d.line(P=P, X1=C, X2=O+V, stroke="#00000020", **kwargs),
107
+ e3d.line(P=P, X1=C, X2=O+U, stroke="#00000020", **kwargs),
108
+ e3d.line(P=P, X1=C, X2=O+V+U, stroke="#00000020", **kwargs)
109
+ ]
110
+
111
+ if 'label_source' in kwargs:
112
+ el += [e3d.text(P=P, X=C, content=kwargs['label_source'], **kwargs)]
113
+ if 'label_detector' in kwargs:
114
+ el += [e3d.text(P=P, X=O, content=kwargs['label_detector'], **kwargs)]
115
+
116
+ if draw_on_detector is not None:
117
+ T_detector = sdg.central_projection_3d
118
+ detector = draw_on_detector(P=P@T_detector, **kwargs)
119
+ else:
120
+ detector = ""
121
+
122
+ return '\n<!-->Source Detector Geometry<-->\n' + detector + '\n '.join(el) + '\n</g>\n'
123
+
124
+
125
+ def svg_homogeneous_line(l, composer: Composer, stroke="yellow", **kwargs):
126
+ """Draw a 2D line given in homogeneous coordinates.
127
+ Note: composer is passed in automatically via svg.Renderer.
128
+ """
129
+ w, h = composer.image_size
130
+ l = pgu.cvec(l)
131
+ x1, y1, x2, y2 = pgu.intersectLineWithRect(l, w, h)
132
+ if not all(isinstance(v, float) for v in [x1, y1, x2, y2]):
133
+ return ""
134
+ return e2d.line(x1=x1, y1=y1, x2=x2, y2=y2, composer=composer, stroke=stroke, **kwargs)
135
+
136
+
137
+ def svg_pluecker_line(P, L, **kwargs):
138
+ """Draw a 3D line given in plucker coordinates."""
139
+ l = pluecker.project(L, P)
140
+ return svg_homogeneous_line(l, **kwargs)
141
+
@@ -0,0 +1,238 @@
1
+ """
2
+ Utility functions for geometry.
3
+
4
+ Note that these functions are imported directly to projective_geometry.
5
+
6
+ Example (compute line through point [2, 1] pointing right):
7
+ import projective_geometry as pg
8
+ pg.join(RP2Point(1, 2, 1), RP2Point(0, 1, 0))
9
+
10
+ Author: André Aichert
11
+ Date: June 22, 2023
12
+
13
+ FIXME (?) numpy is awkward with keeping track of row versus column vectors.
14
+ Possibly simplify code to just use 1D arrays?
15
+
16
+ """
17
+
18
+ import numpy as np
19
+ from numpy.linalg import pinv
20
+ import shlex
21
+
22
+
23
+ def dot(a,b):
24
+ """Computes dot product for two vectors, no matter the actual shape of the
25
+ numpy array (e.g. 2D row or 2D column or 1D) """
26
+ return np.dot(a.ravel(), b.ravel())
27
+
28
+
29
+ def cvec(vector):
30
+ """Column vector from 1D array or list of values."""
31
+ vector = np.array(vector)
32
+ if vector.ndim == 1:
33
+ vector = vector.reshape(-1, 1)
34
+ return vector
35
+
36
+
37
+ def nullspace(M):
38
+ """ Solve M@X == 0 for |X| == 1.
39
+ Returns solution closest to 0 if M is full rank"""
40
+ _, _, V = np.linalg.svd(M)
41
+ return V[-1, :]
42
+
43
+
44
+ def append(vector, last_coordinate):
45
+ """"Takes a vector as a list of values or an np.array, interprets it as a
46
+ column vector and appends the value last_coordinate to the end of it."""
47
+ vector = cvec(vector)
48
+ return np.vstack([vector, [last_coordinate]])
49
+
50
+
51
+ def homogenize(euclidean):
52
+ """"Takes a vector as a list of values or an np.array, interprets it as a
53
+ column vector and appends a one to the end of it."""
54
+ return append(euclidean, 1.0)
55
+
56
+
57
+ def infinite(direction):
58
+ """"Takes a vector as a list of values or an np.array, interprets it as a
59
+ column vector and appends a zero to the end of it. Normalized to unit."""
60
+ return append(direction, 0.0) / np.linalg.norm(direction)
61
+
62
+
63
+ def dehomogenize(vector):
64
+ """"Divides column vector by last element and returns all but last element.
65
+ """
66
+ vector = cvec(vector)
67
+ return vector[0:-1] / vector[-1]
68
+
69
+
70
+ def hessianNormalForm(vector):
71
+ """Compute the Hessian normal form of a 2D line or 3D plane given as
72
+ homogeneous three-, respectively four-vector."""
73
+ return vector / np.linalg.norm(vector[0:-1])
74
+
75
+
76
+ def RP2Point(x, y, w=1):
77
+ """Functions to improve code readability"""
78
+ return cvec([x,y,w])
79
+
80
+
81
+ def RP2Line(l0, l1, l2):
82
+ """Functions to improve code readability"""
83
+ return cvec([l0, l1, l2])
84
+
85
+
86
+ def RP3Point(x, y, z, w=1):
87
+ """Functions to improve code readability"""
88
+ return cvec([x,y,z,w])
89
+
90
+
91
+ def RP3Plane(p0, p1, p2, p3):
92
+ """Functions to improve code readability"""
93
+ return cvec([p0, p1, p2, p3])
94
+
95
+
96
+ def join2(a,b):
97
+ """Compute joining line from two homogeneous 2D points."""
98
+ return cvec(np.cross(cvec(a)[:,0], cvec(b)[:,0]))
99
+
100
+
101
+ def meet2(l,m):
102
+ """Compute intersection from homogeneous coordinates of two 2D lines."""
103
+ return cvec(np.cross(cvec(l)[:,0], cvec(m)[:,0]))
104
+
105
+
106
+ def join3(A, B, C):
107
+ """
108
+ Compute the common plane passing through three points or the point of
109
+ intersection of three planes.
110
+
111
+ Note: for computing the joining line from two 3D points, please use
112
+ L = pluecker.join_points(A,B)
113
+
114
+ Parameters:
115
+ A, B, C: 1D arrays or column vectors representing three points
116
+ in homogeneous coordinates.
117
+ """
118
+ ABC = np.vstack((A, B, C)) if A.ndim == 1 else np.hstack((A, B, C))
119
+ P = np.array([
120
+ +np.linalg.det(ABC[[1, 2, 3], :]),
121
+ -np.linalg.det(ABC[[0, 2, 3], :]),
122
+ +np.linalg.det(ABC[[0, 1, 3], :]),
123
+ -np.linalg.det(ABC[[0, 1, 2], :]),
124
+ ])
125
+ return cvec(P)
126
+
127
+
128
+ def meet3(P, Q, R):
129
+ """
130
+ Compute the point of intersection for three planes that meet at one point.
131
+
132
+ Note: for computing the line of intersection from two 3D planes, please use
133
+ L = pluecker.meet_planes(P,Q)
134
+
135
+ Parameters:
136
+ P, Q, R: 1D arrays or column vectors representing planes in homogeneous
137
+ coordinates.
138
+ """
139
+ PQR = np.vstack((P, Q, R)) if P.ndim == 1 else np.hstack((P, Q, R))
140
+ X = np.array([
141
+ +np.linalg.det(PQR[[1, 2, 3], :]),
142
+ -np.linalg.det(PQR[[0, 2, 3], :]),
143
+ +np.linalg.det(PQR[[0, 1, 3], :]),
144
+ -np.linalg.det(PQR[[0, 1, 2], :]),
145
+ ])
146
+ return cvec(X)
147
+
148
+
149
+ def KRt(K, R, t):
150
+ """Compose projection matrix from intrinsic and extrinsic parameters."""
151
+ return K@np.column_stack((R,t))
152
+
153
+
154
+ def line2d_to_angle_intercept(l):
155
+ """Convert homogeneous coordinates of a 2D line to ange and intercept.
156
+ As is the convention in mathematics, the angle is measured with respect
157
+ to the x-axis. alpha=t=0 corresponds to the line (0,1,0,0)."""
158
+ alpha = np.arctan2(l[1], l[0]) - np.pi * 0.5 # angle
159
+ alpha = alpha + 2.0 * np.pi if alpha < -np.pi else alpha
160
+ t = -l[2] / np.sqrt(l[0] * l[0] + l[1] * l[1]) # intercept
161
+ return np.array([alpha, t])
162
+
163
+
164
+ def line2d_from_angle_intercept(alpha, t):
165
+ """Convert ange and intercept to homogeneous coordinates of a 2D line.
166
+ As is the convention in mathematics, the angle is measured with respect
167
+ to the x-axis. alpha=t=0 corresponds to the line (0,1,0,0)."""
168
+ return RP2Line(
169
+ np.cos(alpha + np.pi * 0.5),
170
+ np.sin(alpha + np.pi * 0.5),
171
+ -t)
172
+
173
+
174
+ def intersectLineWithRect(l, n_x: float, n_y: float ):
175
+ """Intersects line with a rect. Use this for drawing lines.
176
+ Arguments:
177
+ l a 2D line in homogeneous coordinates.
178
+ sequence of numbers or np.array.
179
+ Returns:
180
+ x1 y1 x2 y2 line entry (x1,y1) and exit points (x2,y2)."""
181
+
182
+ eps = 1e-10
183
+ l = cvec(l)
184
+ # Find intersections with image boundaries
185
+ intersection = [
186
+ meet2(l,cvec([1,0,0]))[:,0],
187
+ meet2(l,cvec([-1,0,n_x-1]))[:,0],
188
+ meet2(l,cvec([0,1,0]))[:,0],
189
+ meet2(l,cvec([0,-1,n_y-1]))[:,0]
190
+ ]
191
+
192
+ # Find intersections which are in bounds
193
+ pto, pfrom = [-1,-1,-1], [-1,-1,-1]
194
+ for i in range(4):
195
+ if abs(intersection[i][2])>eps:
196
+ intersection[i] = intersection[i] / intersection[i][2]
197
+ if intersection[i][0]<=n_x+eps and intersection[i][1]<=n_y+eps and \
198
+ intersection[i][0]+eps>=0 and intersection[i][1]+eps>=0:
199
+ if pfrom[0]<0:
200
+ pfrom = [intersection[i][0],intersection[i][1],0]
201
+ elif pto[0]<0:
202
+ pto = [intersection[i][0],intersection[i][1],0]
203
+ else:
204
+ # This may happen if a corner coincides with the line.
205
+ # Then, we have to use two intersections, which are far apart to get the line.
206
+ pto2=cvec([intersection[i][0],intersection[i][1],0])
207
+ if np.linalg.norm(np.array(pfrom)-np.array(pto)) < np.linalg.norm(np.array(pfrom)-np.array(pto2)):
208
+ pto=pto2
209
+
210
+ return (pfrom[0],pfrom[1],pto[0],pto[1])
211
+
212
+
213
+ def parse_ompl(ompl):
214
+ """Load a text file with one matrix per line (*.ompl).
215
+ Lines that so not start with '#' character (for comments) contain a colon
216
+ seperated list of floating point values, each representing a matrix row.
217
+ Lines starting with '#>' may contain additional meta info, such as
218
+ #> spacing="0.1" detector_size_px="800 600"
219
+ """
220
+ comments = []
221
+ meta = dict()
222
+ matrices = []
223
+ for index, line in enumerate(ompl.split('\n')):
224
+ if line.startswith('#'):
225
+ if line.startswith('#>'):
226
+ # Expected string: key1:"value1" key2:"value2" ...
227
+ kvps = [assignment.split('=') for assignment in shlex.split(line[2:])]
228
+ meta.update({kvp[0]: kvp[1] for kvp in kvps})
229
+ else:
230
+ comments += (index, line[2:])
231
+ else:
232
+ if len(line) < 2:
233
+ continue
234
+ line = line.replace('[', '')
235
+ line = line.replace(']', '')
236
+ matrices += [[[float(value) for value in row.split()] for row in line.split(';')]]
237
+ return matrices, meta, comments
238
+
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.4
2
+ Name: ProjectiveGeometry23
3
+ Version: 0.1.0
4
+ Summary: Projective geometry in 2D and 3D with homogeneous and Plücker coordinates, projection matrices, and visualization.
5
+ Home-page: https://github.com/aaichert/ProjectiveGeometry23
6
+ Author: Andre Aichert
7
+ Author-email: aaichert@gmail.com
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.7
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: numpy
16
+ Requires-Dist: scipy
17
+ Provides-Extra: svg
18
+ Requires-Dist: svg_snip; extra == "svg"
19
+ Dynamic: author
20
+ Dynamic: author-email
21
+ Dynamic: classifier
22
+ Dynamic: description
23
+ Dynamic: description-content-type
24
+ Dynamic: home-page
25
+ Dynamic: license
26
+ Dynamic: license-file
27
+ Dynamic: provides-extra
28
+ Dynamic: requires-dist
29
+ Dynamic: requires-python
30
+ Dynamic: summary
31
+
32
+ # ProjectiveGeometry23
33
+ ## Projective Geometry of Two- and Three-space
34
+
35
+ `ProjectiveGeometry23` is a collection of numpy-based utilities for projective geometry of real two- and three-space, including homogeneous coordinates of point, lines and planes, Plücker coordinates and projection matrices.
36
+
37
+ The package has been converted from an existing C++ implementation [LibProjectiveGeometry](https://github.com/aaichert/LibProjectiveGeometry), which is well-tested and obviously faster.
38
+
39
+ The main use of this code is for the visualization or X-Ray source-detector geometries but applied generally to geometric computer vision problems.
40
+
41
+ Features:
42
+ - Computing with points, lines and planes
43
+ - Plücker coordinates, Plücker matrices
44
+ - Projection matrices, intrinsic and extrinsic parameters
45
+ - Decomposition of projection matrices, backprojection
46
+ - Visualization of X-Ray source-detector geometries
47
+ - (WIP) estimation based on direct linear transform, X-ray calibraion
48
+
49
+ General recommendation for better readibility of outputs:
50
+
51
+ ```py
52
+ from rich import print
53
+ np.set_printoptions(suppress=True)
54
+ ```
55
+
56
+ Example of an interactive visualization, using the optional `svg_snip` package:
57
+
58
+ ```py
59
+ from svg_snip.Jupyter import CanvasWithOverlay
60
+ from svg_snip.Composer import Composer
61
+ import svg_snip.Elements as e2d
62
+ import svg_snip.Elements3D as e3d
63
+
64
+ from ProjectiveGeometry23.homography import rotation_x, rotation_z, scale
65
+ from ProjectiveGeometry23.svg_utils import svg_source_detector, svg_world_geometry
66
+
67
+ vis = CanvasWithOverlay(int(target.image_size[0]), int(target.image_size[1]))
68
+
69
+ # World transformation
70
+ ax, az = 0.5, 0.5
71
+ s = 0.2
72
+
73
+ def handle_draw(vis):
74
+ global ax
75
+ global az
76
+ x,y = vis.mouse_state.pos()
77
+ svg = Composer((vis.w, vis.h))
78
+
79
+ svg.add(svg_world_geometry)
80
+ svg.add(svg_source_detector, projection=target,
81
+ draw_on_detector=svg_world_geometry,
82
+ label_source='C0', label_detector='I0(u,v)')
83
+
84
+ svg.add(e2d.star, x=x, y=y, size=8,
85
+ fill="red" if vis.mouse_state.clicked else "blue")
86
+
87
+ svg.add(e2d.text, x=10, y=20, content=f'ax={ax:.3} az={az:.3}')
88
+
89
+ T = scale(s) @ rotation_x(ax) @ rotation_z(az)
90
+ raw_svg_code = svg.render(P=target.P@T)
91
+ vis.html_overlay.value = raw_svg_code
92
+
93
+ if vis.mouse_state.clicked:
94
+ az += vis.mouse_state.dx * 0.01
95
+ ax += vis.mouse_state.dy * 0.01
96
+
97
+ vis.handle_draw = handle_draw
98
+
99
+ vis.display()
100
+
101
+ ```
102
+
103
+ ![source_detector_geometry.png](source_detector_geometry.png)
104
+
105
+ ## Installation
106
+
107
+ ### Using pip
108
+ You can install `ProjectiveGeometry23` using pip:
109
+
110
+ ```bash
111
+ pip install ProjectiveGeometry23
112
+ ```
113
+
114
+
115
+ ### Using `setup.py`
116
+
117
+ ```sh
118
+ git clone https://github.com/aaichert/ProjectiveGeometry23
119
+ cd ProjectiveGeometry23
120
+ python setup.py install
121
+ ```
122
+
123
+ ### Testing and Publication on PyPy
124
+
125
+ Two useful code snippets
126
+
127
+ ```bash
128
+ python -m unittest discover tests
129
+ ```
130
+
131
+ ```bash
132
+ python setup.py sdist bdist_wheel
133
+ pip install twine
134
+ twine upload dist/*
135
+ ```
136
+
137
+ ### References
138
+
139
+ 1. LibProjectiveGeometry (c++). GitHub https://github.com/aaichert/EpipolarConsistency/tree/master/code/LibProjectiveGeometry
140
+ 2. Hartley, Richard, and Andrew Zisserman. Multiple view geometry in computer vision. Cambridge university press, 2003. https://www.robots.ox.ac.uk/~vgg/hzbook/
141
+ 3. Coxeter, Harold Scott Macdonald. Projective geometry. Springer Science & Business Media, 2003.
142
+ 4. Stolfi, Jorge. "Oriented projective geometry." Proceedings of the third annual symposium on Computational geometry. 1987.
@@ -0,0 +1,13 @@
1
+ ProjectiveGeometry23/__init__.py,sha256=nEYkor_6Nf-o7BiMinE-QURrXcIQtJbsyd613uX6Pfs,57
2
+ ProjectiveGeometry23/central_projection.py,sha256=yh12BpCTF9SK2bUH14lP4zWhbr-G_luCOcVY10SGC5M,9718
3
+ ProjectiveGeometry23/estimation.py,sha256=3-xccEYAG4J9ojlxeHx2rEBfFKIwcFhMsqaCmi-D5mw,2286
4
+ ProjectiveGeometry23/homography.py,sha256=P0B96CaeOccoWRDHHShFLsIea0wSdkn-1XHvLmtisEs,2896
5
+ ProjectiveGeometry23/pluecker.py,sha256=ayanNyDbwxwCGdC2bEXAGq3kjTQRXTtgticK7s9zABY,6586
6
+ ProjectiveGeometry23/source_detector_geometry.py,sha256=MBE45j9uVqk8rW-PPABKNJRT_QthfZX2yuvcz6_zIxg,5130
7
+ ProjectiveGeometry23/svg_utils.py,sha256=-spwByvZKlSoTnH1cX5VpZ8AhI0jAVHB8Mg1HUm5GD0,4988
8
+ ProjectiveGeometry23/utils.py,sha256=p6ZowiTDr0yvo_TTCF8mxtt3e-dFoEDw53pLYJ__hiY,8108
9
+ projectivegeometry23-0.1.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
10
+ projectivegeometry23-0.1.0.dist-info/METADATA,sha256=KvhGzS8uoNT_Qez2Xxz9rCOiHUu2kqJHDdyvKNTvDBM,4394
11
+ projectivegeometry23-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ projectivegeometry23-0.1.0.dist-info/top_level.txt,sha256=WwClYIS9b_oVIm0Xkr6hB-A8-W8UgFOLW3yQQM1mx2g,21
13
+ projectivegeometry23-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+